Panel Performa 400% lebih cepat melalui persepsi kinerja

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

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

Contoh berikut akan memandu Anda menggunakan panel Performa.

Menyiapkan dan membuat ulang skenario pembuatan profil

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

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

Setelah penyiapan selesai, skenario yang akan dibuat profil harus dibuat ulang dan direkam. Agar tidak bingung, 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 sebagai panel perf mulai dari sini dan seterusnya—mengamati instance DevTools pertama untuk membuat ulang skenario, yang memuat profil.

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

Keadaan awal: mengidentifikasi peluang untuk perbaikan

Setelah pemuatan selesai, hal berikut di instance panel perf kedua terlihat di screenshot berikutnya. Fokus pada aktivitas thread utama, yang terlihat di bawah jalur berlabel Utama. Dapat dilihat bahwa ada lima kelompok aktivitas besar dalam flame chart. Tugas ini terdiri dari tugas-tugas yang paling banyak memakan waktu pemuatan. Total waktu tugas ini adalah sekitar 10 detik. Dalam screenshot berikut, panel performa digunakan untuk berfokus pada masing-masing grup aktivitas ini untuk melihat apa yang dapat ditemukan.

Screenshot panel performa di DevTools yang memeriksa pemuatan rekaman aktivitas performa di panel performa instance DevTools lainnya. Perlu waktu sekitar 10 detik untuk memuat profil. Waktu ini sebagian besar dibagi menjadi lima kelompok aktivitas utama.

Kelompok aktivitas pertama: tugas yang tidak perlu

Terlihat jelas bahwa grup aktivitas pertama adalah kode lama yang masih berjalan, tetapi tidak terlalu dibutuhkan. Pada dasarnya, segala sesuatu di bawah blok hijau berlabel processThreadEvents adalah sia-sia. Saya memenangkannya dengan cepat. Menghapus panggilan fungsi tersebut dapat menghemat waktu sekitar 1,5 detik. Keren!

Grup aktivitas kedua

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

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

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

Screenshot memory profiler di DevTools yang menilai konsumsi memori 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 menginvestigasi. Dalam panel Memory, jenis pembuatan profil "Allocation sampling" dipilih, yang merekam cuplikan heap untuk panel perf yang memuat profil CPU.

Screenshot status awal memory profiler. Opsi 'allocation sampling' ditandai dengan kotak merah, dan menunjukkan bahwa opsi ini terbaik untuk pembuatan profil memori JavaScript.

Screenshot berikut menampilkan cuplikan heap yang dikumpulkan.

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

Dari cuplikan heap ini, diamati bahwa class Set menggunakan banyak memori. Dengan memeriksa titik panggilan, ditemukan bahwa kita menetapkan properti jenis Set yang tidak perlu ke objek yang dibuat dalam volume besar. Biaya ini bertambah dan banyak memori yang terpakai, sehingga biasanya aplikasi mengalami error pada input yang besar.

Set berguna untuk menyimpan item unik dan menyediakan operasi yang menggunakan keunikan kontennya, seperti menghapus duplikat set data dan memberikan 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 diubah dari Set menjadi array biasa. Setelah menerapkan perubahan ini, snapshot heap lain diambil, dan terjadi pengurangan alokasi memori. Meskipun tidak mencapai peningkatan kecepatan yang cukup besar dengan perubahan ini, manfaat sekundernya adalah aplikasi lebih jarang mengalami error.

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

Kelompok aktivitas ketiga: menimbang kompromi struktur data

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

Dalam implementasi fungsi appendEventAtLevel, ada satu hal yang terlihat berbeda. Untuk setiap entri data tunggal dalam input (yang dikenal dalam kode sebagai "peristiwa"), item ditambahkan ke peta yang melacak posisi vertikal entri linimasa. Hal ini menimbulkan masalah, karena jumlah item yang disimpan sangat besar. Maps cepat untuk pencarian berbasis kunci, tetapi keuntungan ini tidak gratis. Ketika peta semakin besar, penambahan data ke peta dapat menjadi, misalnya, menjadi mahal karena pengulangan. Biaya ini akan kentara jika sejumlah besar item ditambahkan ke peta secara berurutan.

/**
 * 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 dibuat ke fungsi appendEventAtLevel. Total waktu untuk menjalankan fungsi adalah 1.372,51 milidetik.

Setelah:

Screenshot panel performa setelah pengoptimalan dilakukan ke fungsiaddEventAtLevel. Total waktu untuk menjalankan fungsi adalah 207,2 milidetik.

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

Dengan memperbesar jendela ini, terlihat bahwa ada dua blok panggilan fungsi yang hampir sama. Dengan melihat nama fungsi yang dipanggil, Anda dapat menyimpulkan bahwa blok ini terdiri dari kode yang membangun hierarki (misalnya, dengan nama seperti refreshTree atau buildChildren). Bahkan, kode terkait adalah kode yang membuat tampilan hierarki di panel samping bawah panel. Menariknya, tampilan hierarki ini tidak ditampilkan tepat setelah dimuat. Sebagai gantinya, pengguna harus memilih tampilan hierarki (tab "Bottom-up", "Pohon Panggilan", dan "Log Peristiwa" di panel samping) agar hierarki ditampilkan. Selain itu, seperti yang dapat Anda ketahui dari screenshot, proses pembuatan pohon dijalankan dua kali.

Screenshot panel performa yang menampilkan beberapa tugas berulang yang dijalankan meskipun tidak diperlukan. Tugas ini dapat ditangguhkan untuk dieksekusi sesuai permintaan, bukan sebelumnya.

Ada dua masalah yang kita identifikasi dengan gambar ini:

  1. Tugas yang tidak penting telah menghambat performa waktu pemuatan. Pengguna tidak selalu memerlukan output-nya. 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.

Kami mulai dengan menunda penghitungan hierarki hingga saat pengguna membuka tampilan hierarki secara manual. Dengan begitu, membuat pohon-pohon ini sepadan dengan biaya yang harus dibayar. Total waktu operasi ini dua kali adalah sekitar 3,4 detik, jadi menundanya membuat perbedaan waktu pemuatan yang signifikan. Kami juga masih mempertimbangkan untuk meng-cache jenis tugas ini.

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 di flame chart, 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 mendalam.

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 ini terjadi beberapa kali, tetapi tentu saja tidak harus terjadi 6 kali! Bahkan, output kode harus tetap terbaru jika tidak ada profil lain yang dimuat. Secara teori, kode seharusnya hanya berjalan sekali.

Setelah diselidiki, kami mendapati bahwa kode terkait dipanggil sebagai konsekuensi dari beberapa bagian dalam pipeline pemuatan secara langsung atau tidak langsung memanggil fungsi yang menghitung minimap. Hal ini karena kompleksitas grafik panggilan program berkembang dari waktu ke waktu, dan lebih banyak dependensi yang ditambahkan ke kode ini tanpa disadari. Tidak ada perbaikan cepat untuk masalah ini. Cara mengatasinya 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 menerapkan ini, kita mendapatkan tampilan linimasa berikut:

Screenshot panel performa yang menunjukkan enam panggilan fungsi terpisah untuk menghasilkan minimap trace 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 ikhtisar di bagian atas panel, dan satu lagi untuk menu {i>drop-down<i} yang memilih profil yang saat ini terlihat dari riwayat (setiap item dalam menu ini berisi ikhtisar profil yang dipilihnya). Meskipun demikian, keduanya memiliki konten yang sama persis, jadi salah satunya harus dapat digunakan kembali untuk yang lain.

Karena minimap ini adalah gambar yang digambar di kanvas, cukup gunakan utilitas kanvas drawImage, lalu jalankan kode sekali saja untuk menghemat waktu tambahan. Sebagai hasil dari upaya ini, durasi grup dikurangi dari 2,4 detik menjadi 140 milidetik.

Kesimpulan

Setelah menerapkan semua perbaikan ini (dan beberapa perbaikan kecil lainnya 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 kini memerlukan waktu sekitar dua detik.

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

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

Takeaway

Ada beberapa pelajaran yang dapat diambil dari hasil ini dalam hal 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 Anda saat aplikasi berjalan, terutama untuk mengidentifikasi peluang guna meningkatkan performa. Panel Performance di Chrome DevTools adalah pilihan tepat untuk aplikasi web karena merupakan alat pembuatan profil web asli di browser, dan selalu diperbarui secara aktif dengan fitur platform web terbaru. Selain itu, sekarang jauh lebih cepat! 😉

Gunakan sampel yang dapat digunakan sebagai beban kerja representatif dan lihat apa yang dapat Anda temukan.

2. Menghindari hierarki panggilan yang kompleks

Bila memungkinkan, hindari membuat grafik panggilan Anda terlalu rumit. Dengan hierarki panggilan yang kompleks, sangat mudah untuk menyebabkan regresi performa dan sulit untuk memahami mengapa kode Anda berjalan sebagaimana mestinya, sehingga sulit untuk mendapatkan peningkatan.

3. Mengidentifikasi pekerjaan yang tidak perlu

Biasanya codebase yang lama hanya berisi kode yang tidak diperlukan lagi. Dalam kasus kami, kode lama dan yang tidak perlu memerlukan banyak bagian dari total waktu pemuatan. Melepaskannya adalah hal yang paling tergantung.

4. Menggunakan struktur data dengan tepat

Menggunakan struktur data untuk mengoptimalkan performa, tetapi juga memahami biaya dan kompromi dari setiap jenis struktur data saat memutuskan struktur mana yang akan digunakan. Hal 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 tersebut mahal untuk dijalankan, sebaiknya simpan hasilnya untuk digunakan pada saat berikutnya. Hal ini juga masuk akal untuk melakukan ini jika operasi dilakukan berkali-kali—bahkan jika setiap waktu tidak terlalu mahal.

6. Menunda pekerjaan yang tidak penting

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

7. Menggunakan algoritma yang efisien pada input yang besar

Untuk {i>input<i} yang besar, algoritma kompleksitas waktu yang optimal menjadi sangat penting. Kami tidak melihat ke dalam kategori tersebut dalam contoh ini, namun nilai penting mereka hampir tidak dilebih-lebihkan.

8. Bonus: menjalankan benchmark pada pipeline Anda

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