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 kesuksesan aplikasi. Salah satu cara untuk melakukannya adalah dengan memeriksa aktivitas aplikasi 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. Dengan memahami aktivitas ini, Anda dapat 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 meningkatkan performa panel Performa. Secara khusus, kami ingin alat ini memuat data performa dalam volume 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, profilnya dapat dibuat menggunakan panel Performa. Untuk membuat profil panel ini sendiri, Anda dapat 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 perf 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 dari panel performa yang memuat profil.

Status awal: mengidentifikasi peluang peningkatan

Setelah pemuatan selesai, berikut ini pada instance panel perf kedua kita diamati pada screenshot berikutnya. Fokus pada aktivitas thread utama, yang terlihat di bawah jalur berlabel Main. Dapat dilihat bahwa ada lima grup besar aktivitas dalam diagram api. Ini terdiri dari tugas yang memuat paling banyak waktu. Total waktu tugas ini adalah 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 rekaman aktivitas performa di panel performa instance DevTools lain. 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

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

Grup aktivitas kedua

Dalam grup aktivitas kedua, solusinya tidak sesederhana dengan yang pertama. buildProfileCalls memerlukan waktu sekitar 0,5 detik, dan tugas tersebut tidak dapat 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 penasaran, kami mengaktifkan opsi Memory 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 sekitar waktu buildProfileCalls dijalankan, yang menunjukkan potensi kebocoran memori.

Screenshot profiler memori di DevTools yang menilai konsumsi memori panel performa. Pemeriksa menyarankan 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 Memori, jenis pembuatan profil "Pengambilan sampel alokasi" dipilih, yang merekam snapshot heap untuk panel performa yang memuat profil CPU.

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

Screenshot berikut menunjukkan snapshot heap yang dikumpulkan.

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

Dari snapshot heap ini, diamati bahwa class Set menghabiskan banyak memori. Dengan memeriksa titik panggilan, ditemukan bahwa kita tidak perlu menetapkan properti jenis Set ke objek yang dibuat dalam volume besar. Biaya ini bertambah dan banyak memori yang digunakan, sehingga 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. 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 yang dikurangi diamati. Meskipun tidak mencapai peningkatan kecepatan yang signifikan dengan perubahan ini, manfaat sekundernya adalah aplikasi 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 bersifat aneh: Anda dapat melihat dalam diagram lingkaran api bahwa bagian ini terdiri dari kolom yang sempit tetapi tinggi, yang menunjukkan panggilan fungsi mendalam, dan rekursi mendalam dalam hal ini. Secara total, 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 hal ini dapat menjadi bottleneck

Di dalam implementasi fungsi appendEventAtLevel, ada satu hal yang menarik. Untuk setiap entri data dalam input (yang dikenal dalam kode sebagai "peristiwa"), item telah ditambahkan ke peta yang melacak posisi vertikal entri linimasa. Hal ini bermasalah, karena jumlah item yang disimpan sangat besar. Peta cepat untuk pencarian berbasis kunci, tetapi keuntungan ini tidak gratis. Saat peta bertambah besar, menambahkan data ke dalamnya dapat menjadi mahal karena pengulangan. Biaya ini menjadi terlihat saat banyak 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 kita menambahkan item di peta untuk setiap entri dalam diagram api. Peningkatannya signifikan, yang mengonfirmasi bahwa bottleneck memang terkait dengan overhead yang ditimbulkan dengan menambahkan semua data ke peta. Waktu yang diperlukan grup aktivitas menyusut dari sekitar 1,4 detik menjadi sekitar 200 milidetik.

Sebelum:

Screenshot panel performa sebelum pengoptimalan dilakukan pada fungsi appendEventAtLevel. 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 adalah 207,2 milidetik.

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

Dengan memperbesar jendela ini, dapat dilihat 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 membuat hierarki (misalnya, dengan nama seperti refreshTree atau buildChildren). Faktanya, kode terkait adalah kode yang membuat tampilan hierarki di panel samping bawah. 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 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 ditangguhkan untuk dijalankan sesuai permintaan, bukan sebelumnya.

Ada dua masalah yang teridentifikasi pada gambar ini:

  1. Tugas non-kritis 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 hierarki dihitung dua kali, meskipun data tidak berubah.

Kita mulai dengan menunda penghitungan hierarki hingga saat pengguna membuka tampilan hierarki secara manual. Hanya dengan begitu Anda dapat membayar harga untuk membuat pohon ini. Total waktu untuk menjalankannya dua kali adalah sekitar 3,4 detik, sehingga menundanya akan membuat perbedaan yang signifikan dalam waktu pemuatan. Kami juga masih mempelajari cara meng-cache jenis tugas ini.

Grup aktivitas kelima: hindari hierarki panggilan yang kompleks jika memungkinkan

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

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

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

Setelah diselidiki, ditemukan bahwa kode terkait dipanggil sebagai konsekuensi dari beberapa bagian dalam pipeline pemuatan yang secara langsung atau tidak langsung memanggil fungsi yang menghitung minimap. Hal ini karena kompleksitas grafik panggilan program berkembang seiring waktu, dan lebih banyak dependensi ke kode ini ditambahkan 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 tetap tidak berubah. Setelah menerapkannya, kita mendapatkan tampilan linimasa ini:

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

Perhatikan bahwa eksekusi rendering minimap 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 dapat digunakan kembali untuk yang lain.

Karena minimap ini adalah gambar yang digambar di kanvas, Anda hanya perlu menggunakan utilitas kanvas drawImage, 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 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 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 pada awalnya adalah kuncinya, dan panel performa adalah alat yang tepat untuk ini.

Penting juga untuk menyoroti bahwa angka ini khusus untuk profil yang digunakan sebagai subjek studi. Profil ini menarik bagi kami karena sangat 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 dalam hal pengoptimalan performa aplikasi Anda:

1. Menggunakan 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 secara aktif dikelola agar selalu mengikuti fitur platform web terbaru. Selain itu, sekarang prosesnya jauh lebih cepat. 😉

Gunakan sampel yang dapat digunakan sebagai beban kerja perwakilan 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, regresi performa mudah terjadi dan sulit untuk memahami alasan kode Anda berjalan seperti itu, sehingga sulit untuk mendapatkan peningkatan.

3. Mengidentifikasi pekerjaan yang tidak perlu

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

4. Menggunakan struktur data dengan tepat

Gunakan struktur data untuk mengoptimalkan performa, tetapi pahami juga biaya dan kompromi yang ditimbulkan oleh setiap jenis struktur data saat memutuskan struktur data mana yang akan digunakan. Hal ini tidak 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 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 outputnya benar-benar diperlukan.

7. Menggunakan algoritma yang efisien pada input besar

Untuk input yang besar, algoritma kompleksitas waktu yang optimal menjadi sangat 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 demikian, Anda dapat secara proaktif mengidentifikasi regresi dan meningkatkan keandalan secara keseluruhan, sehingga Anda dapat meraih kesuksesan jangka panjang.