Deteksi Memory Leak — Debugging

Deteksi Memory Leak Memory leak terjadi ketika aplikasi mengalokasikan memori tetapi tidak pernah melepaskannya kembali. Seiring waktu, pemakaian memori terus…

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.

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.

  1. Buka halamannya di jendela Incognito, supaya extension browser gak ikut tercatat.
  2. DevTools, tab Memory, pilih Heap snapshot, klik Take snapshot. Ini patokannya.
  3. Lakukan aksi yang dicurigai beberapa kali, misalnya buka-tutup panel Notifikasi 5 kali, lalu ambil snapshot kedua.
  4. Ulangi aksinya, lalu ambil snapshot ketiga.
  5. 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

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
}

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

⚠️ Jebakan yang sering ketemu

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).

Yang akan kamu pelajari