Pengantar
Meskipun JavaScript menggunakan pembersihan sampah memori untuk pengelolaan memori otomatis, JavaScript bukanlah pengganti pengelolaan memori yang efektif dalam aplikasi. Aplikasi JavaScript mengalami masalah terkait memori yang sama seperti aplikasi native, seperti kebocoran memori dan bloat, tetapi aplikasi tersebut juga harus menangani jeda pembersihan sampah memori. Aplikasi skala besar seperti Gmail mengalami masalah yang sama dengan aplikasi Anda yang lebih kecil. Baca terus untuk mempelajari cara tim Gmail menggunakan Chrome DevTools untuk mengidentifikasi, mengisolasi, dan memperbaiki masalah memori mereka.
Sesi Google I/O 2013
Kami mempresentasikan materi ini di Google I/O 2013. Lihat video di bawah:
Gmail, ada masalah…
Tim Gmail menghadapi masalah serius. Anekdot tentang tab Gmail yang menghabiskan beberapa gigabyte memori di laptop dan desktop yang memiliki keterbatasan resource semakin sering terdengar, sering kali dengan kesimpulan bahwa tab tersebut membuat seluruh browser tidak berfungsi. Cerita tentang CPU yang terpaku pada 100%, aplikasi yang tidak responsif, dan tab Chrome yang tidak berfungsi ("Dia mati, Jim"). Tim tidak tahu cara memulai mendiagnosis masalah, apalagi memperbaikinya. Mereka tidak tahu seberapa luas masalahnya dan alat yang tersedia tidak dapat diskalakan ke aplikasi besar. Tim ini bergabung dengan tim Chrome, dan bersama-sama mereka mengembangkan teknik baru untuk melakukan triage masalah memori, meningkatkan kualitas alat yang ada, dan memungkinkan pengumpulan data memori dari lapangan. Namun, sebelum membahas alat tersebut, mari kita bahas dasar-dasar pengelolaan memori JavaScript.
Dasar-Dasar Pengelolaan Memori
Sebelum dapat mengelola memori secara efektif di JavaScript, Anda harus memahami dasar-dasarnya. Bagian ini akan membahas jenis primitif, grafik objek, dan memberikan definisi untuk pembengkakan memori secara umum dan kebocoran memori di JavaScript. Memori di JavaScript dapat dikonseptualisasikan sebagai grafik dan karenanya Teori grafik berperan dalam pengelolaan memori JavaScript dan Heap Profiler.
Jenis Primitif
JavaScript memiliki tiga jenis primitif:
- Angka (mis. 4, 3,14159)
- Boolean (benar atau salah)
- String ("Hello World")
Jenis primitif ini tidak dapat mereferensikan nilai lain. Dalam grafik objek, nilai ini selalu berupa node daun atau node akhir, yang berarti nilai tersebut tidak pernah memiliki tepi keluar.
Hanya ada satu jenis penampung: Objek. Di JavaScript, Objek adalah array asosiatif. Objek yang tidak kosong adalah node dalam dengan tepi keluar ke nilai (node) lain.
Bagaimana dengan Array?
Array di JavaScript sebenarnya adalah Objek yang memiliki kunci numerik. Ini adalah penyederhanaan, karena runtime JavaScript akan mengoptimalkan Objek seperti Array dan menampilkannya di balik layar sebagai array.
Terminologi
- Nilai - Instance dari jenis primitif, Objek, Array, dll.
- Variabel - Nama yang mereferensikan nilai.
- Properti - Nama dalam Objek yang mereferensikan nilai.
Grafik Objek
Semua nilai dalam JavaScript adalah bagian dari grafik objek. Grafik dimulai dengan root, misalnya, objek jendela. Anda tidak dapat mengontrol masa aktif root GC karena root GC dibuat oleh browser dan dihancurkan saat halaman di-unload. Variabel global sebenarnya adalah properti di jendela.

Kapan Nilai Menjadi Sampah?
Nilai menjadi sampah jika tidak ada jalur dari root ke nilai. Dengan kata lain, dimulai dari root dan menelusuri semua properti dan variabel Objek yang aktif di frame stack secara menyeluruh, nilai tidak dapat dijangkau, nilai tersebut telah menjadi sampah.

Apa yang dimaksud dengan Kebocoran Memori di JavaScript?
Kebocoran memori di JavaScript biasanya terjadi saat ada node DOM yang tidak dapat dijangkau dari hierarki DOM halaman, tetapi masih direferensikan oleh objek JavaScript. Meskipun browser modern semakin mempersulit terjadinya kebocoran secara tidak sengaja, hal ini masih lebih mudah daripada yang dibayangkan. Misalnya, Anda menambahkan elemen ke hierarki DOM seperti ini:
email.message = document.createElement("div");
displayList.appendChild(email.message);
Kemudian, Anda menghapus elemen dari daftar tampilan:
displayList.removeAllChildren();
Selama email
ada, elemen DOM yang dirujuk oleh pesan tidak akan dihapus, meskipun sekarang elemen tersebut dilepaskan dari hierarki DOM halaman.
Apa yang dimaksud dengan Bloat?
Halaman Anda membengkak jika Anda menggunakan lebih banyak memori daripada yang diperlukan untuk kecepatan halaman yang optimal. Secara tidak langsung, kebocoran memori juga menyebabkan pemborosan, tetapi hal itu bukan karena desain. Cache aplikasi yang tidak memiliki batas ukuran adalah sumber umum pemborosan memori. Selain itu, halaman Anda dapat membengkak karena data host, misalnya, data piksel yang dimuat dari gambar.
Apa yang dimaksud dengan Pembersihan Sampah Memori?
Pembersihan sampah memori adalah cara memori diklaim kembali di JavaScript. Browser menentukan kapan hal ini terjadi. Selama pengumpulan, semua eksekusi skrip di halaman Anda ditangguhkan sementara nilai live ditemukan oleh penelusuran grafik objek yang dimulai dari root GC. Semua nilai yang tidak dapat dijangkau diklasifikasikan sebagai sampah. Memori untuk nilai sampah diklaim kembali oleh pengelola memori.
Pengumpul Sampah V8 secara Mendetail
Untuk membantu lebih memahami cara kerja pembersihan sampah memori, mari kita lihat pembersihan sampah memori V8 secara mendetail. V8 menggunakan kolektor generasi. Memori dibagi menjadi dua generasi: yang baru dan yang lama. Alokasi dan pengumpulan dalam generasi muda cepat dan sering. Alokasi dan pengumpulan dalam generasi lama lebih lambat dan lebih jarang.
Kolektor Generasional
V8 menggunakan kolektor dua generasi. Usia nilai ditentukan sebagai jumlah byte yang dialokasikan sejak dialokasikan. Dalam praktiknya, usia nilai sering kali diperkirakan dengan jumlah koleksi generasi muda yang bertahan. Setelah nilai cukup lama, nilai tersebut akan dipegang oleh generasi lama.
Dalam praktiknya, nilai yang baru dialokasikan tidak bertahan lama. Studi tentang program Smalltalk menunjukkan bahwa hanya 7% nilai yang bertahan setelah pengumpulan generasi muda. Studi serupa di seluruh runtime menemukan bahwa rata-rata antara 90% dan 70% nilai yang baru dialokasikan tidak pernah memiliki masa berlaku ke generasi lama.
Generasi Muda
Heap generasi muda di V8 dibagi menjadi dua ruang, yang diberi nama dari dan ke. Memori dialokasikan dari ruang ke. Alokasi sangat cepat, hingga ruang ke penuh, pada saat itu pengumpulan generasi muda dipicu. Pengumpulan generasi muda pertama-tama menukar ruang dari dan ke, ruang ke lama (sekarang ruang dari) dipindai dan semua nilai aktif disalin ke ruang ke atau dipegang ke generasi lama. Pengumpulan generasi muda yang khas akan memerlukan waktu sekitar 10 milidetik (mdtk).
Secara intuitif, Anda harus memahami bahwa setiap alokasi yang dilakukan aplikasi akan membuat Anda lebih dekat ke kehabisan ruang dan menyebabkan jeda GC. Developer game, perhatikan: untuk memastikan waktu frame 16 md (diperlukan untuk mencapai 60 frame per detik), aplikasi Anda harus membuat alokasi nol, karena satu koleksi generasi muda akan menghabiskan sebagian besar waktu frame.

Generasi Lama
Heap generasi lama di V8 menggunakan algoritma mark-compact untuk pengumpulan. Alokasi generasi lama terjadi setiap kali nilai memiliki masa berlaku dari generasi muda ke generasi lama. Setiap kali pengumpulan generasi lama terjadi, pengumpulan generasi baru juga dilakukan. Aplikasi Anda akan dijeda dalam hitungan detik. Dalam praktiknya, hal ini dapat diterima karena koleksi generasi lama jarang terjadi.
Ringkasan GC V8
Pengelolaan memori otomatis dengan pembersihan sampah memori sangat bagus untuk produktivitas developer, tetapi setiap kali Anda mengalokasikan nilai, Anda semakin mendekati jeda pembersihan sampah memori. Jeda pembersihan sampah memori dapat merusak nuansa aplikasi Anda dengan menyebabkan jank. Setelah memahami cara JavaScript mengelola memori, Anda dapat membuat pilihan yang tepat untuk aplikasi Anda.
Memperbaiki Gmail
Selama setahun terakhir, banyak fitur dan perbaikan bug yang telah ditambahkan ke Chrome DevTools sehingga membuatnya lebih canggih dari sebelumnya. Selain itu, browser itu sendiri membuat perubahan utama pada performance.memory API sehingga Gmail dan aplikasi lainnya dapat mengumpulkan statistik memori dari kolom. Dengan alat yang luar biasa ini, tugas yang sebelumnya tampak mustahil akan segera menjadi permainan seru untuk melacak pelaku.
Alat dan Teknik
Data Kolom dan performance.memory API
Mulai Chrome 22, performance.memory API diaktifkan secara default. Untuk aplikasi yang berjalan lama seperti Gmail, data dari pengguna yang sebenarnya sangat berharga. Informasi ini memungkinkan kami membedakan antara pengguna berat -- pengguna yang menghabiskan 8-16 jam sehari di Gmail, menerima ratusan pesan sehari -- dari pengguna biasa yang menghabiskan beberapa menit sehari di Gmail, menerima sekitar selusin pesan seminggu.
API ini menampilkan tiga bagian data:
- jsHeapSizeLimit - Jumlah memori (dalam byte) yang dibatasi untuk heap JavaScript.
- totalJSHeapSize - Jumlah memori (dalam byte) yang telah dialokasikan heap JavaScript termasuk ruang kosong.
- usedJSHeapSize - Jumlah memori (dalam byte) yang sedang digunakan.
Satu hal yang perlu diingat adalah API menampilkan nilai memori untuk seluruh proses Chrome. Meskipun bukan mode default, dalam keadaan tertentu, Chrome dapat membuka beberapa tab dalam proses perender yang sama. Artinya, nilai yang ditampilkan oleh performance.memory dapat berisi jejak memori tab browser lain selain tab yang berisi aplikasi Anda.
Mengukur Memori dalam Skala Besar
Gmail melengkapi JavaScript-nya untuk menggunakan performance.memory API guna mengumpulkan informasi memori sekitar sekali setiap 30 menit. Karena banyak pengguna Gmail membiarkan aplikasi berjalan selama berhari-hari, tim dapat melacak pertumbuhan memori dari waktu ke waktu serta statistik jejak memori secara keseluruhan. Dalam beberapa hari setelah melakukan instrumentasi Gmail untuk mengumpulkan informasi memori dari sampel pengguna secara acak, tim memiliki cukup data untuk memahami seberapa luas masalah memori di kalangan pengguna rata-rata. Mereka menetapkan dasar pengukuran dan menggunakan aliran data yang masuk untuk melacak progres menuju sasaran pengurangan konsumsi memori. Pada akhirnya, data ini juga akan digunakan untuk mendeteksi regresi memori.
Selain tujuan pelacakan, pengukuran lapangan juga memberikan insight yang tajam tentang korelasi antara jejak memori dan performa aplikasi. Berbeda dengan kepercayaan umum bahwa "lebih banyak memori menghasilkan performa yang lebih baik", tim Gmail menemukan bahwa semakin besar jejak memori, semakin lama latensi untuk tindakan Gmail umum. Dengan mengetahui hal ini, mereka semakin termotivasi untuk mengurangi konsumsi memori.

Mengidentifikasi Masalah Memori dengan Linimasa DevTools
Langkah pertama dalam memecahkan masalah performa adalah membuktikan bahwa masalah tersebut ada, membuat pengujian yang dapat direproduksi, dan melakukan pengukuran dasar pengukuran masalah. Tanpa program yang dapat direproduksi, Anda tidak dapat mengukur masalah dengan andal. Tanpa pengukuran dasar pengukuran, Anda tidak akan tahu seberapa banyak performa Anda meningkat.
Panel Linimasa DevTools adalah kandidat yang ideal untuk membuktikan bahwa masalah tersebut ada. Laporan ini memberikan ringkasan lengkap tentang waktu yang dihabiskan saat memuat dan berinteraksi dengan aplikasi atau halaman web Anda. Semua peristiwa, mulai dari memuat resource hingga mengurai JavaScript, menghitung gaya, menjeda pengumpulan sampah, dan mewarnai ulang diplot pada linimasa. Untuk menyelidiki masalah memori, panel Linimasa juga memiliki mode Memori yang melacak total memori yang dialokasikan, jumlah node DOM, jumlah objek jendela, dan jumlah pemroses peristiwa yang dialokasikan.
Membuktikan bahwa masalah ada
Mulailah dengan mengidentifikasi urutan tindakan yang Anda curigai mengalami kebocoran memori. Mulai rekam linimasa, lalu lakukan urutan tindakan. Gunakan tombol tempat sampah di bagian bawah untuk memaksa pembersihan sampah memori penuh. Jika, setelah beberapa iterasi, Anda melihat grafik berbentuk gergaji, berarti Anda mengalokasikan banyak objek yang berumur pendek. Namun, jika urutan tindakan tidak diharapkan menghasilkan memori yang dipertahankan, dan jumlah node DOM tidak turun kembali ke dasar pengukuran tempat Anda memulai, Anda memiliki alasan yang kuat untuk mencurigai adanya kebocoran.

Setelah mengonfirmasi bahwa masalah tersebut ada, Anda bisa mendapatkan bantuan untuk mengidentifikasi sumber masalah dari Heap Profiler DevTools.
Menemukan Kebocoran Memori dengan Heap Profiler DevTools
Panel Profiler menyediakan profiler CPU dan profiler Heap. Profil heap berfungsi dengan mengambil snapshot grafik objek. Sebelum snapshot diambil, generasi lama dan baru akan dibersihkan. Dengan kata lain, Anda hanya akan melihat nilai yang aktif saat snapshot diambil.
Ada terlalu banyak fungsi di Profiler heap untuk dibahas secara memadai dalam artikel ini, tetapi dokumentasi mendetail dapat ditemukan di situs Developer Chrome. Di sini, kita akan berfokus pada profiler Alokasi Heap.
Menggunakan Profiler Alokasi Heap
Profiler Alokasi Heap menggabungkan informasi snapshot mendetail dari Heap Profiler dengan pembaruan dan pelacakan inkremental panel Linimasa. Buka panel Profil, mulai profil Record Heap Allocations, lakukan urutan tindakan, lalu hentikan perekaman untuk analisis. Profiler alokasi mengambil snapshot heap secara berkala selama perekaman (seringnya setiap 50 md!) dan satu snapshot akhir di akhir perekaman.

Batang di bagian atas menunjukkan kapan objek baru ditemukan di heap. Tinggi setiap batang sesuai dengan ukuran objek yang baru dialokasikan, dan warna batang menunjukkan apakah objek tersebut masih aktif atau tidak dalam snapshot heap akhir: batang biru menunjukkan objek yang masih aktif di akhir linimasa, batang abu-abu menunjukkan objek yang dialokasikan selama linimasa, tetapi telah dihapus oleh garbage collector.
Pada contoh di atas, tindakan dilakukan 10 kali. Program contoh meng-cache lima objek, sehingga lima batang biru terakhir diharapkan. Namun, panel biru paling kiri menunjukkan potensi masalah. Kemudian, Anda dapat menggunakan penggeser di linimasa di atas untuk memperbesar snapshot tertentu dan melihat objek yang baru-baru ini dialokasikan pada titik tersebut. Mengklik objek tertentu di heap akan menampilkan hierarki retensinya di bagian bawah snapshot heap. Dengan memeriksa jalur retensi ke objek, Anda akan mendapatkan informasi yang cukup untuk memahami alasan objek tidak dikumpulkan, dan Anda dapat melakukan perubahan kode yang diperlukan untuk menghapus referensi yang tidak diperlukan.
Menyelesaikan Krisis Memori Gmail
Dengan menggunakan alat dan teknik yang telah dibahas di atas, tim Gmail dapat mengidentifikasi beberapa kategori bug: cache tanpa batas, array callback yang terus bertambah dan menunggu sesuatu terjadi yang sebenarnya tidak pernah terjadi, dan pemroses peristiwa yang tidak sengaja mempertahankan targetnya. Dengan memperbaiki masalah ini, penggunaan memori Gmail secara keseluruhan berkurang secara drastis. Pengguna dalam persentase 99% menggunakan memori 80% lebih sedikit dari sebelumnya dan konsumsi memori pengguna median menurun hampir 50%.

Karena Gmail menggunakan lebih sedikit memori, latensi jeda GC berkurang, sehingga meningkatkan pengalaman pengguna secara keseluruhan.
Selain itu, dengan tim Gmail mengumpulkan statistik tentang penggunaan memori, mereka dapat menemukan regresi pembersihan sampah di dalam Chrome. Secara khusus, dua bug fragmentasi ditemukan saat data memori Gmail mulai menunjukkan peningkatan yang signifikan dalam kesenjangan antara total memori yang dialokasikan dan memori aktif.
Pesan Ajakan
Tanyakan pada diri Anda pertanyaan-pertanyaan berikut:
- Berapa banyak memori yang digunakan aplikasi saya? Anda mungkin menggunakan terlalu banyak memori yang bertentangan dengan kepercayaan umum yang memiliki dampak negatif bersih pada performa aplikasi secara keseluruhan. Sulit untuk mengetahui angka yang tepat, tetapi pastikan untuk memverifikasi bahwa cache tambahan yang digunakan halaman Anda memiliki dampak performa yang dapat diukur.
- Apakah halaman saya bebas kebocoran? Jika halaman Anda mengalami kebocoran memori, hal ini tidak hanya dapat memengaruhi performa halaman, tetapi juga tab lainnya. Gunakan pelacak objek untuk membantu mempersempit kebocoran.
- Seberapa sering halaman saya melakukan GC? Anda dapat melihat jeda GC menggunakan panel Linimasa di Chrome Developer Tools. Jika halaman Anda sering melakukan GC, kemungkinan Anda terlalu sering mengalokasikan, sehingga menghabiskan memori generasi muda.
Kesimpulan
Kita memulainya dalam krisis. Membahas dasar-dasar inti pengelolaan memori di JavaScript dan V8 secara khusus. Anda telah mempelajari cara menggunakan alat ini, termasuk fitur pelacak objek baru yang tersedia di build Chrome terbaru. Dengan pengetahuan ini, tim Gmail berhasil menyelesaikan masalah penggunaan memori dan meningkatkan performa. Anda dapat melakukan hal yang sama dengan aplikasi web Anda.