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, mengoptimalkan performanya dan memastikan aplikasi dimuat dengan cepat serta menawarkan interaksi yang lancar sangat penting untuk pengalaman pengguna dan keberhasilan aplikasi. Salah satu cara untuk melakukannya adalah dengan memeriksa aktivitas aplikasi menggunakan alat pembuatan profil untuk melihat apa yang terjadi di balik layarnya saat berjalan selama jangka waktu tertentu. Panel Performa di DevTools adalah alat pembuatan profil yang sangat baik untuk menganalisis dan mengoptimalkan performa aplikasi web. Jika aplikasi Anda berjalan di Chrome, Anda akan mendapatkan ringkasan visual mendetail tentang aktivitas browser saat aplikasi Anda dieksekusi. Memahami aktivitas ini dapat membantu Anda mengidentifikasi pola, hambatan, dan titik aktif performa yang dapat Anda tindak lanjuti 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 berperforma lebih baik. Khususnya, kami ingin memuat volume data performa yang besar dengan lebih cepat. Hal ini terjadi, misalnya, saat memprofilkan proses yang berjalan lama atau kompleks atau mengambil data dengan perincian tinggi. Untuk mencapai hal ini, pemahaman tentang cara aplikasi berjalan dan alasan aplikasi berjalan seperti itu diperlukan terlebih dahulu, yang dicapai dengan menggunakan alat pembuatan profil.

Seperti yang mungkin Anda ketahui, DevTools sendiri adalah aplikasi web. Oleh karena itu, performanya dapat diprofilkan menggunakan panel Performance. Untuk memprofilkan panel ini sendiri, Anda dapat membuka DevTools, lalu membuka instance DevTools lain yang terlampir padanya. Di Google, penyiapan ini dikenal sebagai DevTools-on-DevTools.

Setelah penyiapan siap, skenario yang akan diprofilkan 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 selanjutnya akan disebut panel perf—mengamati instance DevTools pertama untuk membuat ulang skenario, yang memuat profil.

Di instance DevTools kedua, rekaman live dimulai, sementara di instance pertama, profil dimuat dari file di disk. File berukuran besar dimuat untuk memprofilkan performa pemrosesan input berukuran besar secara akurat. Saat kedua instance selesai dimuat, data pembuatan profil performa—yang biasa disebut rekaman aktivitas—akan terlihat di instance DevTools kedua dari panel perf yang memuat profil.

Kondisi awal: mengidentifikasi peluang peningkatan

Setelah pemuatan selesai, hal berikut pada instance panel performa kedua kami diamati pada screenshot berikutnya. Fokus pada aktivitas thread utama, yang terlihat di bagian jalur berlabel Main. Dapat dilihat bahwa ada lima grup besar aktivitas dalam flame chart. Bagian ini terdiri dari tugas yang pemuatannya membutuhkan waktu paling lama. Total waktu untuk menyelesaikan tugas ini adalah sekitar 10 detik. Pada 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 rekaman aktivitas performa di panel performa instance DevTools lainnya. Profil memerlukan waktu sekitar 10 detik untuk dimuat. Waktu ini sebagian besar dibagi ke dalam lima grup aktivitas utama.

Grup aktivitas pertama: pekerjaan yang tidak perlu

Ternyata grup aktivitas pertama adalah kode lama yang masih berjalan, tetapi sebenarnya tidak diperlukan. Pada dasarnya, semua yang ada di bawah blok hijau berlabel processThreadEvents adalah upaya yang sia-sia. Yang itu adalah solusi cepat. Menghapus panggilan fungsi tersebut menghemat waktu sekitar 1,5 detik. Keren!

Grup aktivitas kedua

Dalam grup aktivitas kedua, solusinya tidak sesederhana grup pertama. buildProfileCalls membutuhkan waktu sekitar 0,5 detik, dan tugas tersebut tidak dapat 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 Memori di panel performa untuk menyelidiki lebih lanjut, dan melihat bahwa aktivitas buildProfileCalls juga menggunakan banyak memori. Di sini, Anda dapat melihat bagaimana grafik garis biru tiba-tiba melonjak di sekitar waktu buildProfileCalls dijalankan, yang menunjukkan potensi kebocoran memori.

Screenshot profiler memori 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 Memori (panel lain di DevTools, berbeda dengan panel Memori di panel performa) untuk menyelidiki. Dalam panel Memory, jenis pembuatan profil "Allocation sampling" dipilih, yang merekam snapshot heap untuk panel perf yang memuat profil CPU.

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

Screenshot berikut menunjukkan snapshot heap yang dikumpulkan.

Screenshot profiler memori, dengan operasi berbasis Set yang menguras memori dipilih.

Dari snapshot heap ini, diamati bahwa class Set menggunakan banyak memori. Dengan memeriksa titik panggilan, ditemukan bahwa kita tidak perlu menetapkan properti jenis Set ke objek yang dibuat dalam volume besar. Biaya ini terus bertambah dan banyak memori yang digunakan, hingga aplikasi sering 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 menyediakan pencarian yang lebih efisien. Namun, fitur tersebut tidak diperlukan karena data yang disimpan dijamin unik dari sumbernya. Oleh karena itu, 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 alokasi memori yang lebih kecil diamati. Meskipun tidak mencapai peningkatan kecepatan yang signifikan dengan perubahan ini, manfaat sekundernya adalah aplikasi menjadi lebih jarang mengalami error.

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

Grup aktivitas ketiga: mempertimbangkan kompromi struktur data

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

Di dalam implementasi fungsi appendEventAtLevel, ada satu hal yang menarik. 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. Peta cepat untuk pencarian berbasis kunci, tetapi keunggulan ini tidak gratis. Seiring bertambah besarnya peta, penambahan data ke peta tersebut dapat menjadi mahal, misalnya karena melakukan penghasahan ulang. Biaya ini akan terasa saat 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 di peta untuk setiap entri dalam flame chart. Peningkatannya signifikan, yang mengonfirmasi bahwa hambatan memang terkait dengan overhead yang terjadi akibat menambahkan semua data ke peta. Waktu yang dibutuhkan grup aktivitas berkurang dari sekitar 1,4 detik menjadi sekitar 200 milidetik.

Sebelum:

Screenshot panel performa sebelum pengoptimalan dilakukan pada fungsi appendEventAtLevel. Total waktu yang dibutuhkan fungsi untuk berjalan adalah 1.372,51 milidetik.

Setelah:

Screenshot panel performa setelah pengoptimalan dilakukan pada fungsi appendEventAtLevel. Total waktu yang diperlukan untuk menjalankan fungsi adalah 207,2 milidetik.

Grup aktivitas keempat: menunda pekerjaan yang tidak penting dan menyimpan data cache untuk mencegah pekerjaan duplikat

Dengan memperbesar 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 membangun hierarki (misalnya, dengan nama seperti refreshTree atau buildChildren). Faktanya, kode terkait adalah kode yang membuat tampilan hierarki di panel laci bawah. Yang menarik adalah tampilan hierarki ini tidak ditampilkan tepat setelah pemuatan. Sebagai gantinya, pengguna perlu memilih tampilan hierarki (tab "Bottom-up", "Call Tree", dan "Event Log" di panel) agar hierarki ditampilkan. Selain itu, seperti yang dapat Anda lihat dari screenshot, proses pembuatan hierarki dijalankan dua kali.

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

Ada dua masalah yang kami identifikasi pada gambar ini:

  1. Tugas yang tidak penting menghambat performa waktu pemuatan. Pengguna tidak selalu memerlukan outputnya. Dengan demikian, tugas ini tidak penting untuk pemuatan profil.
  2. Hasil tugas ini tidak di-cache. Itulah sebabnya pohon dihitung dua kali, meskipun datanya tidak berubah.

Kami memulai dengan menunda penghitungan hierarki saat pengguna membuka tampilan hierarki secara manual. Baru kemudian kita akan merasa bahwa harga yang dibayarkan untuk menanam pohon ini sepadan. Total waktu untuk menjalankan ini dua kali adalah sekitar 3,4 detik, jadi menundanya membuat perbedaan yang signifikan dalam waktu pemuatan. Kami juga masih menyelidiki cara menyimpan tugas jenis ini dalam cache.

Grup aktivitas kelima: hindari hierarki panggilan yang rumit jika memungkinkan

Dengan melihat grup ini secara cermat, jelas bahwa rantai panggilan tertentu dipanggil berulang kali. Pola yang sama muncul 6 kali di berbagai tempat dalam diagram flame, dan total durasi jendela ini sekitar 2,4 detik.

Screenshot panel performa yang menampilkan enam panggilan fungsi terpisah untuk membuat peta mini rekaman aktivitas yang sama, yang masing-masing memiliki tumpukan panggilan yang dalam.

Kode terkait yang dipanggil beberapa kali adalah bagian yang memproses data untuk dirender di "peta mini" (ringkasan aktivitas linimasa di bagian atas panel). Tidak jelas mengapa hal itu terjadi beberapa kali, tetapi yang pasti hal itu tidak perlu terjadi 6 kali. Faktanya, output kode akan tetap terbaru jika tidak ada profil lain yang dimuat. Secara teori, kode hanya boleh dijalankan satu kali.

Setelah diselidiki, ditemukan bahwa kode terkait dipanggil sebagai akibat dari beberapa bagian dalam pipeline pemuatan yang secara langsung atau tidak langsung memanggil fungsi yang menghitung peta mini. 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 mengatasinya bergantung pada arsitektur codebase yang dimaksud. Dalam kasus ini, kami harus mengurangi sedikit kompleksitas hierarki panggilan dan menambahkan pemeriksaan untuk mencegah eksekusi kode jika data input tidak berubah. Setelah menerapkan ini, kami mendapatkan tampilan linimasa berikut:

Screenshot panel performa yang menampilkan enam panggilan fungsi terpisah untuk membuat peta mini rekaman aktivitas yang sama, yang 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 dipilihnya). Namun demikian, kedua file ini memiliki konten yang sama persis, sehingga salah satunya dapat digunakan kembali untuk file lainnya.

Karena peta mini ini adalah gambar yang digambar di kanvas, kita hanya perlu menggunakan drawImage utilitas kanvas, lalu menjalankan kode hanya sekali untuk menghemat waktu. 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 pada linimasa pemuatan profil terlihat sebagai 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 peningkatan sekitar 80% dicapai dengan upaya yang relatif rendah, karena sebagian besar yang dilakukan terdiri dari perbaikan cepat. Tentu saja, mengidentifikasi apa yang harus dilakukan pada awalnya sangat penting, dan panel performa adalah alat yang tepat untuk hal ini.

Penting juga untuk menekankan bahwa angka-angka ini khusus untuk profil yang digunakan sebagai subjek studi. Profil tersebut menarik bagi kami karena ukurannya sangat besar. Namun 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. Gunakan alat pembuatan profil untuk mengidentifikasi pola performa runtime

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

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

2. Hindari hierarki panggilan yang rumit

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

3. Mengidentifikasi pekerjaan yang tidak perlu

Codebase yang sudah lama biasanya berisi kode yang tidak lagi diperlukan. Dalam kasus kami, kode lama dan tidak perlu mengambil sebagian besar total waktu pemuatan. Menghapusnya adalah hal yang paling mudah dilakukan.

4. Menggunakan struktur data dengan tepat

Gunakan struktur data untuk mengoptimalkan performa, tetapi juga pahami biaya dan kompromi yang ditimbulkan oleh setiap jenis struktur data saat memutuskan struktur data 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 tugas duplikat untuk operasi yang kompleks atau berulang

Jika operasi mahal untuk dieksekusi, sebaiknya simpan hasilnya untuk saat berikutnya diperlukan. Hal ini juga masuk akal jika operasi dilakukan berkali-kali—meskipun setiap waktu individual tidak terlalu mahal.

6. Menunda pekerjaan yang tidak penting

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

7. Menggunakan algoritma yang efisien pada input besar

Untuk input besar, algoritma kompleksitas waktu yang optimal menjadi sangat penting. Dalam contoh ini, kita tidak membahas kategori ini, tetapi pentingnya tidak dapat dilebih-lebihkan.

8. Bonus: membandingkan performa pipeline Anda

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