Panel Performa 400% lebih cepat melalui persepsi kinerja

Andrés Olivares
Andrés Olivares
Nancy Li
Nancy Li

Terlepas dari jenis aplikasi yang Anda kembangkan, pengoptimalan performanya dan memastikannya dimuat dengan cepat serta menawarkan interaksi yang lancar sangat penting bagi pengalaman pengguna dan keberhasilan aplikasi. Salah satu cara untuk melakukannya adalah dengan memeriksa aktivitas aplikasi dengan menggunakan alat pembuatan profil untuk melihat apa yang terjadi di balik layar saat aplikasi berjalan selama jangka waktu tertentu. Panel Performa di DevTools adalah alat pembuatan profil yang bagus untuk menganalisis dan mengoptimalkan performa aplikasi web. Jika aplikasi Anda berjalan di Chrome, aplikasi ini akan memberi Anda ringkasan visual mendetail mengenai apa yang dilakukan browser saat aplikasi Anda sedang dieksekusi. Memahami aktivitas ini dapat membantu Anda mengidentifikasi pola, bottleneck, dan hotspot performa yang dapat ditindaklanjuti untuk meningkatkan performa.

Contoh berikut memandu Anda menggunakan panel Performa.

Menyiapkan dan membuat ulang skenario pembuatan profil

Baru-baru ini, kami menetapkan sasaran untuk meningkatkan performa panel Performa. Secara khusus, kami ingin memuat data performa dalam jumlah besar dengan lebih cepat. Hal ini terjadi, misalnya, saat membuat profil proses yang berjalan lama atau kompleks atau merekam data dengan perincian tinggi. Untuk mencapai hal ini, pertama-tama diperlukan pemahaman tentang bagaimana performa aplikasi dan alasan performanya seperti itu, yang dicapai dengan menggunakan alat pembuatan profil.

Seperti yang mungkin Anda ketahui, DevTools sendiri adalah aplikasi web. Dengan demikian, profil bisnis dapat dibuat profilnya menggunakan panel Performa. Untuk membuat profil panel ini sendiri, Anda bisa membuka DevTools, lalu membuka instance DevTools lain yang terpasang padanya. Di Google, penyiapan ini dikenal sebagai DevTools-on-DevTools.

Setelah penyiapan siap, skenario yang akan dibuat profilnya harus dibuat ulang dan direkam. Untuk menghindari kebingungan, jendela DevTools asli akan disebut sebagai instance DevTools "pertama", dan jendela yang memeriksa instance pertama akan disebut sebagai "instance DevTools kedua".

Screenshot instance DevTools yang memeriksa elemen di DevTools itu sendiri.
DevTools-on-DevTools: memeriksa DevTools dengan DevTools.

Pada instance DevTools kedua, panel Performance—yang akan disebut panel performa mulai dari sekarang dan seterusnya—mengamati instance DevTools pertama untuk membuat ulang skenario, yang memuat profil.

Pada instance DevTools kedua, rekaman live dimulai, sedangkan pada instance pertama, profil dimuat dari file di disk. File besar dimuat untuk membuat profil performa pemrosesan input besar secara akurat. Saat kedua instance selesai dimuat, data pembuatan profil performa—biasanya disebut rekaman aktivitas—akan terlihat di instance DevTools kedua pada panel perf yang memuat profil.

Status awal: mengidentifikasi peluang untuk perbaikan

Setelah pemuatan selesai, berikut ini pada instance panel perf kedua kita diamati pada screenshot berikutnya. Fokus pada aktivitas thread utama, yang terlihat di bawah trek berlabel Utama. Dapat dilihat bahwa ada lima kelompok aktivitas besar dalam flame chart. Tugas ini terdiri dari tugas yang pemuatannya paling lama. Total waktu untuk menyelesaikan tugas ini sekitar 10 detik. Dalam screenshot berikut, panel performa digunakan untuk berfokus pada setiap grup aktivitas ini untuk melihat apa yang dapat ditemukan.

Screenshot panel performa di DevTools yang memeriksa pemuatan trace performa di panel performa instance DevTools lain. Profil memerlukan waktu sekitar 10 detik untuk dimuat. Waktu ini sebagian besar dibagi menjadi lima kelompok aktivitas utama.

Grup aktivitas pertama: pekerjaan yang tidak perlu

Jelas bahwa grup aktivitas pertama adalah kode lama yang masih berjalan, tetapi tidak benar-benar diperlukan. Pada dasarnya, semua konten di bawah blok hijau berlabel processThreadEvents sia-sia. Yang itu adalah kemenangan cepat. Menghapus panggilan fungsi tersebut dapat menghemat waktu sekitar 1,5 detik. Keren!

Grup aktivitas kedua

Pada kelompok aktivitas kedua, solusinya tidak sesederhana seperti yang pertama. buildProfileCalls memerlukan waktu sekitar 0,5 detik, dan tugas tersebut bukanlah sesuatu yang bisa dihindari.

Screenshot panel performa di DevTools yang memeriksa instance panel performa lain. Tugas yang terkait dengan fungsi buildProfileCalls memerlukan waktu sekitar 0,5 detik.

Karena ingin tahu, kami mengaktifkan opsi Memory di panel perf untuk menyelidiki lebih lanjut, dan melihat bahwa aktivitas buildProfileCalls juga menggunakan banyak memori. Di sini, Anda dapat melihat bagaimana grafik garis biru tiba-tiba melompat ketika buildProfileCalls dijalankan, yang menunjukkan potensi kebocoran memori.

Screenshot memory profiler di DevTools yang menilai konsumsi memori di panel performa. Pemeriksa menunjukkan bahwa fungsi buildProfileCalls bertanggung jawab atas kebocoran memori.

Untuk menindaklanjuti kecurigaan ini, kami menggunakan panel Memory (panel lain di DevTools, berbeda dari panel samping Memory di panel perf) untuk menyelidikinya. Dalam panel Memory, bagian "Allocation sampling" pembuatan profil dipilih, yang merekam snapshot heap untuk panel perf yang memuat profil CPU.

Screenshot status awal memory profiler. 'Pengambilan sampel alokasi' disorot dengan kotak merah, dan ini menunjukkan bahwa opsi ini paling baik untuk pembuatan profil memori JavaScript.

Screenshot berikut menampilkan snapshot heap yang dikumpulkan.

Screenshot memory profiler, dengan operasi berbasis Set yang sarat memori dipilih.

Dari snapshot heap ini, terlihat bahwa class Set menggunakan banyak memori. Dengan memeriksa titik panggilan, diketahui bahwa kita tidak perlu menetapkan properti jenis Set ke objek yang dibuat dalam volume besar. Biaya ini terus bertambah dan banyak memori dikonsumsi, sampai pada titik bahwa aplikasi sering mengalami error pada input yang besar.

Kumpulan berguna untuk menyimpan item unik dan menyediakan operasi yang menggunakan keunikan kontennya, seperti menghapus duplikat set data dan menyediakan pencarian yang lebih efisien. Namun, fitur tersebut tidak diperlukan karena data yang disimpan dijamin unik dari sumbernya. Dengan demikian, set tidak diperlukan sejak awal. Untuk meningkatkan alokasi memori, jenis properti telah diubah dari Set menjadi array biasa. Setelah menerapkan perubahan ini, snapshot heap lain diambil, dan alokasi memori berkurang. Meskipun tidak mencapai peningkatan kecepatan yang signifikan dengan perubahan ini, manfaat sekundernya adalah aplikasi lebih jarang mengalami error.

Screenshot memory profiler. Operasi berbasis Set yang sebelumnya membutuhkan banyak memori telah diubah untuk menggunakan array biasa, yang telah mengurangi biaya memori secara signifikan.

Kelompok aktivitas ketiga: menimbang kompromi struktur data

Bagian ketiga aneh: Anda dapat melihat di flame chart bahwa itu terdiri dari kolom sempit tapi tinggi, yang menunjukkan panggilan fungsi dalam, dan rekursi mendalam dalam kasus ini. Secara total, bagian ini berlangsung sekitar 1,4 detik. Dengan melihat bagian bawah bagian ini, jelas bahwa lebar kolom ini ditentukan oleh durasi satu fungsi: appendEventAtLevel, yang menunjukkan bahwa kolom tersebut bisa menjadi bottleneck

Di dalam implementasi fungsi appendEventAtLevel, satu hal menarik. Untuk setiap entri data dalam input (yang dikenal dalam kode sebagai "peristiwa"), item telah ditambahkan ke peta yang melacak posisi vertikal entri linimasa. Ini bermasalah, karena jumlah item yang disimpan sangat banyak. Maps cepat untuk pencarian berbasis kunci, tetapi keuntungan ini tidak diberikan secara cuma-cuma. Saat peta bertambah besar, menambahkan data ke dalamnya dapat menjadi mahal karena pengulangan. Biaya ini akan terlihat jelas ketika item dalam jumlah besar ditambahkan ke peta secara berturut-turut.

/**
 * Adds an event to the flame chart data at a defined vertical level.
 */
function appendEventAtLevel (event, level) {
  // ...

  const index = data.length;
  data.push(event);
  this.indexForEventMap.set(event, index);

  // ...
}

Kami bereksperimen dengan pendekatan lain yang tidak mengharuskan kami menambahkan item dalam peta untuk setiap entri dalam flame chart. Peningkatannya signifikan, mengonfirmasi bahwa bottleneck memang terkait dengan overhead yang terjadi dengan menambahkan semua data ke peta. Waktu yang dibutuhkan kelompok aktivitas menyusut dari sekitar 1,4 detik menjadi sekitar 200 milidetik.

Sebelum:

Screenshot panel performa sebelum pengoptimalan dilakukan pada fungsi addEventAtLevel. Total waktu untuk menjalankan fungsi tersebut adalah 1.372,51 milidetik.

Setelah:

Screenshot panel performa setelah pengoptimalan dilakukan pada fungsi addEventAtLevel. Total waktu untuk menjalankan fungsi tersebut adalah 207,2 milidetik.

Kelompok aktivitas keempat: menunda pekerjaan non-kritis dan data cache untuk mencegah pekerjaan duplikat

Dengan memperbesar tampilan jendela ini, dapat dilihat bahwa ada dua blok panggilan fungsi yang hampir identik. Dengan melihat nama fungsi yang dipanggil, Anda dapat menyimpulkan bahwa blok ini terdiri dari kode yang merupakan struktur hierarki (misalnya, dengan nama seperti refreshTree atau buildChildren). Bahkan, kode terkait adalah kode yang membuat tampilan hierarki di panel samping bawah panel. Yang menarik adalah tampilan hierarki ini tidak ditampilkan tepat setelah dimuat. Sebagai gantinya, pengguna harus memilih tampilan hierarki (tab "Bottom-up", "Call Tree", dan "Event Log" di panel samping) agar hierarki ditampilkan. Selain itu, seperti yang Anda lihat dari screenshot, proses pembangunan pohon dijalankan dua kali.

Screenshot panel performa yang menampilkan beberapa tugas berulang yang dijalankan meskipun tidak diperlukan. Tugas ini dapat ditunda untuk dijalankan sesuai permintaan, bukan di awal.

Ada dua masalah yang teridentifikasi pada gambar ini:

  1. Tugas non-kritis menghambat performa waktu pemuatan. Pengguna tidak selalu membutuhkan {i>output<i}. Dengan demikian, tugas tersebut tidak penting untuk pemuatan profil.
  2. Hasil tugas ini tidak di-cache. Itulah mengapa pohon dihitung dua kali, meskipun datanya tidak berubah.

Kita mulai dengan menunda penghitungan hierarki hingga pengguna membuka tampilan hierarki secara manual. Hanya dengan begitu Anda dapat membayar harga untuk membuat pohon ini. Total waktu menjalankan dua kali ini adalah sekitar 3,4 detik, jadi menundanya membuat perbedaan waktu pemuatan yang signifikan. Kami juga masih mencari cara untuk menyimpan tugas-tugas ini ke dalam cache.

Grup aktivitas kelima: hindari hierarki panggilan yang kompleks jika memungkinkan

Dengan mengamati grup ini, terlihat jelas bahwa rantai panggilan tertentu dipanggil berulang kali. Pola yang sama muncul 6 kali di tempat yang berbeda dalam bagan api, dan total durasi jendela ini sekitar 2,4 detik!

Screenshot panel performa yang menampilkan enam panggilan fungsi terpisah untuk menghasilkan minimap rekaman aktivitas yang sama, yang masing-masing memiliki stack panggilan dalam.

Kode terkait yang dipanggil beberapa kali adalah bagian yang memproses data yang akan dirender pada "minimap" (ringkasan aktivitas linimasa di bagian atas panel). Tidak jelas mengapa hal itu terjadi beberapa kali, tetapi pasti tidak harus terjadi 6 kali! Bahkan, output kode harus tetap terkini jika tidak ada profil lain yang dimuat. Secara teori, kode hanya boleh berjalan sekali.

Setelah melakukan penyelidikan, kami mendapati bahwa kode terkait dipanggil sebagai konsekuensi dari beberapa bagian dalam pipeline pemuatan secara langsung atau tidak langsung yang memanggil fungsi yang menghitung minimap. Hal ini karena kompleksitas grafik panggilan program berkembang dari waktu ke waktu, dan lebih banyak dependensi pada kode ini ditambahkan tanpa disadari. Tidak ada perbaikan cepat untuk masalah ini. Cara menyelesaikannya bergantung pada arsitektur codebase yang dimaksud. Dalam kasus ini, kita harus sedikit mengurangi kompleksitas hierarki panggilan dan menambahkan pemeriksaan untuk mencegah eksekusi kode jika data input tidak berubah. Setelah menerapkannya, kami mendapatkan perkiraan rentang waktu berikut:

Screenshot panel performa yang menunjukkan enam panggilan fungsi terpisah untuk menghasilkan minimap rekaman aktivitas yang sama dikurangi menjadi hanya dua kali.

Perhatikan bahwa eksekusi rendering peta mini terjadi dua kali, bukan sekali. Hal ini karena ada dua peta mini yang digambar untuk setiap profil: satu untuk ringkasan di bagian atas panel, dan satu lagi untuk menu drop-down yang memilih profil yang saat ini terlihat dari histori (setiap item dalam menu ini berisi ringkasan profil yang dipilih). Meskipun demikian, keduanya memiliki konten yang sama persis, sehingga salah satunya harus dapat digunakan kembali untuk konten lainnya.

Karena kedua peta mini ini digambar di atas kanvas, cukup gunakan utilitas kanvas drawImage, lalu jalankan kode sekali saja untuk menghemat waktu ekstra. Sebagai hasil dari upaya ini, durasi grup dikurangi dari 2,4 detik menjadi 140 milidetik.

Kesimpulan

Setelah menerapkan semua perbaikan ini (dan beberapa perbaikan lain yang lebih kecil di sana-sini), perubahan linimasa pemuatan profil terlihat seperti berikut:

Sebelum:

Screenshot panel performa yang menampilkan pemuatan rekaman aktivitas sebelum pengoptimalan. Proses ini memerlukan waktu sekitar sepuluh detik.

Setelah:

Screenshot panel performa yang menampilkan pemuatan rekaman aktivitas setelah pengoptimalan. Proses ini sekarang memerlukan waktu sekitar dua detik.

Waktu pemuatan setelah peningkatan adalah 2 detik, yang berarti bahwa peningkatan sekitar 80% dicapai dengan upaya yang relatif rendah, karena sebagian besar hal yang dilakukan terdiri dari perbaikan cepat. Tentu saja, mengidentifikasi apa yang harus dilakukan pertama kali dengan tepat adalah kuncinya, dan panel performa adalah alat yang tepat untuk hal ini.

Penting juga untuk menyoroti bahwa angka-angka ini terkait dengan profil yang digunakan sebagai subjek studi. Profil tersebut menarik bagi kami karena ukurannya yang besar. Meskipun demikian, karena pipeline pemrosesan sama untuk setiap profil, peningkatan signifikan yang dicapai berlaku untuk setiap profil yang dimuat di panel performa.

Poin-poin penting

Ada beberapa pelajaran yang dapat diambil dari hasil ini terkait pengoptimalan performa aplikasi Anda:

1. Memanfaatkan alat pembuatan profil untuk mengidentifikasi pola performa runtime

Alat pembuatan profil sangat berguna untuk memahami apa yang terjadi dalam aplikasi saat aplikasi berjalan, terutama untuk mengidentifikasi peluang peningkatan performa. Panel Performance di Chrome DevTools adalah opsi yang bagus untuk aplikasi web karena merupakan alat pembuatan profil web native di browser, dan dipelihara secara aktif agar selalu terbaru dengan fitur platform web terbaru. Selain itu, kini Gemini Advanced jauh lebih cepat. 😉

Gunakan contoh yang dapat digunakan sebagai workload representatif dan lihat apa yang dapat Anda temukan.

2. Menghindari hierarki panggilan yang kompleks

Jika memungkinkan, hindari membuat grafik panggilan yang terlalu rumit. Dengan hierarki panggilan yang kompleks, Anda dapat dengan mudah memperkenalkan regresi performa dan sulit memahami mengapa kode Anda berjalan seperti itu, sehingga sulit untuk mendapatkan peningkatan.

3. Mengidentifikasi pekerjaan yang tidak perlu

Sangatlah umum untuk menempatkan codebase agar berisi kode yang tidak lagi diperlukan. Dalam kasus kami, kode lama dan tidak perlu menghabiskan sebagian besar total waktu pemuatan. Menyingkirkannya adalah buah yang paling tidak mungkin.

4. Menggunakan struktur data dengan tepat

Menggunakan struktur data untuk mengoptimalkan kinerja, tetapi juga memahami biaya dan konsekuensi yang ditimbulkan oleh setiap jenis struktur data saat memutuskan mana yang akan digunakan. Ini bukan hanya kompleksitas ruang dari struktur data itu sendiri, tetapi juga kompleksitas waktu dari operasi yang berlaku.

5. Menyimpan hasil dalam cache untuk menghindari pekerjaan duplikat untuk operasi yang kompleks atau berulang

Jika operasi itu mahal untuk dijalankan, masuk akal untuk menyimpan hasilnya pada saat berikutnya diperlukan. Hal ini juga masuk akal untuk dilakukan jika operasi dilakukan berkali-kali—bahkan jika setiap waktu tidak terlalu mahal.

6. Menunda pekerjaan non-kritis

Jika output tugas tidak segera diperlukan dan eksekusi tugas memperluas jalur kritis, pertimbangkan untuk menundanya dengan memanggilnya secara lambat saat output-nya benar-benar diperlukan.

7. Menggunakan algoritma yang efisien pada input yang besar

Untuk input besar, algoritma kompleksitas waktu yang optimal menjadi penting. Kami tidak mempelajari kategori tersebut dalam contoh ini, tetapi kepentingannya tidak mungkin dilebih-lebihkan.

8. Bonus: menjalankan benchmark pada pipeline Anda

Untuk memastikan kode Anda yang berkembang tetap cepat, sebaiknya pantau perilaku dan membandingkannya dengan standar. Dengan cara ini, Anda secara proaktif mengidentifikasi regresi dan meningkatkan keandalan secara keseluruhan, sehingga Anda siap meraih kesuksesan jangka panjang.