Memory leak adalah memori yang sudah dialokasikan program tapi gak pernah dibebaskan, karena masih ada referensi yang memegangnya. Penyebab-penyebabnya (listener, timer, closure) dibahas di pelajaran Memory Management di jalur Performance. Pelajaran ini soal menemukannya: membuktikan memorinya naik terus, lalu mencari siapa yang masih memegang data yang seharusnya sudah dibuang. Mirip curiga ada pipa bocor di rumah. Kamu tutup semua keran, catat angka meteran air, tunggu, lalu catat lagi. Kalau angkanya terus naik padahal gak ada yang memakai air, berarti bocor. Baru setelah itu kamu telusuri pipanya satu per satu.
🧪 Tebakan cepat: heap di empat snapshot berturut-turut 10, 30, 25, lalu 40 MB. Rata-rata naiknya 10 MB per snapshot. Ini leak atau bukan? Jawabannya ada di bagian "Membaca tren dari angka".
Gejalanya: naik terus, bukan naik sekali
Leak jarang terasa di menit pertama. Tandanya: makin lama aplikasinya dipakai, makin lambat, pemakaian memori tab-nya terus naik, dan lama-lama tab-nya bisa crash. Heap adalah bagian memori tempat JavaScript menyimpan object, array, dan elemen DOM.
- Sehat: memori naik saat bekerja, lalu turun saat garbage collector (GC) membuang yang gak dipakai. Grafiknya seperti gigi gergaji.
- Bocor: tiap kali aksi yang sama diulang, memori naik dan gak pernah turun lagi ke angka awal. Grafiknya seperti tangga.
- Bukan leak: lonjakan sesaat yang turun lagi setelah halaman selesai dimuat, atau memori yang besar sejak awal tapi angkanya stabil.
Alat pertama untuk memastikan: Task Manager Chrome (Shift+Esc di Windows dan Linux, menu Window lalu Task Manager di Mac). Klik kanan judul kolom untuk menyalakan kolom JavaScript memory, lalu pakai halamannya beberapa menit. Kalau angkanya terus naik, lanjut ke tab Memory.
Tiga snapshot di tab Memory
Heap snapshot adalah potret semua object yang ada di heap pada satu saat. Mengambil snapshot selalu menjalankan GC dulu, jadi yang tercatat hanya yang masih dipegang.
- Buka halamannya di jendela Incognito, supaya extension browser gak ikut tercatat.
- DevTools, tab Memory, pilih Heap snapshot, klik Take snapshot. Ini patokannya.
- Lakukan aksi yang dicurigai beberapa kali, misalnya buka-tutup panel Notifikasi 5 kali, lalu ambil snapshot kedua.
- Ulangi aksinya, lalu ambil snapshot ketiga.
- Pilih snapshot terakhir, ganti tampilannya dari Summary ke Comparison, dan bandingkan dengan snapshot sebelumnya.
Ini Comparison di Lab halaman ini, setelah panel dibuka-tutup 6 kali di antara snapshot 2 dan 3:
| Class | # New | # Deleted | # Delta | Size Delta |
|---|---|---|---|---|
Array |
6 | 0 | +6 | +6 MB |
Detached <ul> |
6 | 0 | +6 | +1,2 kB |
Detached <li> |
300 | 0 | +300 | +30 kB |
- # New dan # Deleted: berapa object kelas itu yang dibuat dan dibuang di antara dua snapshot. # Delta selisihnya, Size Delta selisih ukurannya.
- Panelnya ditutup 6 kali, dan ada tepat 6
<ul>yang gak pernah dibuang. Angka yang sama dengan jumlah aksimu itu tanda paling kuat. - Kalau gak bocor, # New dan # Deleted-nya sama, dan # Delta-nya 0.
Detached node dan Retainers
Detached DOM node adalah elemen yang sudah dikeluarkan dari halaman (misalnya dengan remove()), tapi masih direferensikan dari JavaScript, jadi GC gak bisa membuangnya. Ketik Detached di kotak Class filter untuk menyaringnya. Klik salah satu barisnya, dan panel Retainers di bawah menunjukkan rantai siapa yang memegangnya. Di Lab, rantainya berakhir di array riwayat:
const riwayat = [];
function tutupPanel(panel) {
panel.remove(); // keluar dari halaman...
riwayat.push(panel); // ...tapi tetap dipegang array: jadi detached
}
function tutupPanelFixed(panel) {
panel.remove();
riwayat.push(panel.dataset.id); // simpan id-nya saja, elemennya boleh dibuang
}
panel.remove()cuma mencabut elemen dari halaman. Elemennya baru bisa dibuang kalau gak ada lagi variabel, array, atau closure yang menyimpannya.riwayat.push(panel)menyimpan elemennya beserta 50<li>di dalamnya dan datanya. Satu panel jadi tertahan utuh.- Ada dua ukuran di heap snapshot. Shallow size: ukuran object itu sendiri. Retained size: memori yang ikut bebas kalau object itu dibuang.
<ul>di Lab shallow size-nya sekitar 200 byte, tapi retained size-nya sekitar 1 MB karena dia memegang datanya. Urutkan berdasarkan Retained size untuk menemukan yang paling mahal.
Membaca tren dari angka
Latihan meringkas deretan snapshot jadi satu keputusan. Kenaikan per langkah dihitung dari snapshot ke snapshot. Leak artinya naik di setiap langkah: satu penurunan saja membatalkannya. growthRate adalah rata-rata kenaikannya, dibulatkan ke bilangan bulat. severity bernilai "none" kalau gak bocor, "low" kalau growthRate di bawah 5, dan "high" kalau 5 atau lebih.
function detectLeaks(memorySnapshots) {
if (memorySnapshots.length < 2) return { hasLeak: false, growthRate: 0, severity: "none" };
let naikTerus = true;
let total = 0;
for (let i = 1; i < memorySnapshots.length; i++) {
const delta = memorySnapshots[i].heapUsed - memorySnapshots[i - 1].heapUsed;
if (delta <= 0) naikTerus = false; // satu kali gak naik membatalkan tren
total += delta;
}
const growthRate = Math.round(total / (memorySnapshots.length - 1));
if (!naikTerus) return { hasLeak: false, growthRate, severity: "none" };
return { hasLeak: true, growthRate, severity: growthRate < 5 ? "low" : "high" };
}
| Heap (MB) | Kenaikan | Naik terus? | growthRate |
Hasil |
|---|---|---|---|---|
| 10, 15, 21, 28 | 5, 6, 7 | ya | 18 / 3 = 6 | hasLeak: true, "high" |
| 10, 12, 14 | 2, 2 | ya | 4 / 2 = 2 | hasLeak: true, "low" |
| 10, 30, 25, 40 | 20, -5, 15 | gak | 30 / 3 = 10 | hasLeak: false, "none" |
Baris terakhir itu jawaban tebakannya: bukan leak menurut aturan ini. Rata-ratanya memang naik 10 MB, tapi ada satu penurunan, jadi trennya gak konsisten. Di Lab, buka-tutup panel 5 kali, snapshot, 6 kali, snapshot, 7 kali, snapshot, dan heap-nya persis 10, 15, 21, 28 MB seperti baris pertama.
Function kedua latihan, findRetainedObjects, menyaring object yang retainCount-nya (jumlah referensi yang memegangnya) lebih dari 1, lalu mengembalikan namanya saja: objects.filter((o) => o.retainCount > 1).map((o) => o.name). Dari EventListener (3), DOMNode (1), dan Timer (2), hasilnya EventListener,Timer. Ini aturan latihan yang disederhanakan: di heap snapshot sungguhan, object yang dipegang satu referensi pun tetap tertahan, dan yang menentukan bocor atau gak adalah apakah referensi itu seharusnya sudah dilepas.
💡 Alat lain di tab Memory
- Allocations on timeline: merekam alokasi selama kamu berinteraksi. Batang biru yang tetap ada sampai rekaman selesai adalah object yang gak pernah dibuang, dan kamu bisa melihat di detik keberapa dia dibuat.
- Detached elements (di Chrome versi baru): langsung mendaftar elemen detached yang masih dipegang JavaScript, tanpa perlu menyaring snapshot.
⚠️ Jebakan yang sering ketemu
- Menyimpulkan dari satu kenaikan. Snapshot kedua yang lebih besar bisa cuma cache atau data yang dimuat sekali. Ulangi aksinya dan ambil tiga snapshot.
- Percaya rata-rata. Di latihan,
10, 30, 25, 40rata-ratanya naik, tapi satu penurunan bikin hasilnyahasLeak: falsedan"none". Cekdelta <= 0di setiap langkah. - Salah batas severity.
growthRatetepat 5 sudah"high". PakaigrowthRate < 5 ? "low" : "high". - Mengukur dengan extension aktif. Extension ikut menaruh object di heap dan mengacaukan perbandingan. Pakai jendela Incognito.
TL;DR: Memory leak adalah memori yang dialokasikan tapi gak pernah dibebaskan, tandanya memori naik terus dan aplikasi makin lambat seiring waktu. Pastikan dulu di Task Manager Chrome, lalu ambil tiga heap snapshot di tab Memory (patokan, setelah aksi, setelah aksi lagi) dan bandingkan dengan tampilan Comparison: kelas yang # Delta-nya sama dengan jumlah aksimu adalah tersangka. Ketik Detached di Class filter untuk menemukan elemen yang sudah di-remove tapi masih dipegang, lalu ikuti panel Retainers sampai ke variabel yang memegangnya. Di latihan, leak berarti naik di setiap snapshot, growthRate adalah rata-rata kenaikan yang dibulatkan, dan severity-nya "none", "low" (di bawah 5), atau "high" (5 ke atas).