Memory Management — Performance

Memory Management di JavaScript Memory leak adalah salah satu penyebab tersembunyi dari performance degradasi. Halaman yang awalnya cepat bisa jadi semakin…

JavaScript membersihkan memori sendiri: garbage collector (GC) membuang data yang sudah gak bisa dijangkau lagi oleh kodemu. Masalahnya muncul kalau kode masih memegang sesuatu yang sebenarnya sudah gak dipakai. Data itu gak bisa dibuang, dan memorinya terus menumpuk. Ini disebut memory leak. Bayangin kamar kos yang tiap minggu dibersihkan petugas. Petugasnya cuma membuang barang yang gak ada pemiliknya. Kalau kamu menempel label "punyaku" di kardus bekas lalu lupa, kardusnya gak pernah dibuang, dan kamarnya makin sempit.

🧪 Tebakan cepat: sebuah modal memegang data 2 MB. Kamu sudah melepas listener-nya saat modal ditutup, tapi lupa clearInterval. Modalnya dibuka-tutup 5 kali. Heap yang awalnya 8 MB jadi berapa: tetap 8 MB, 10,5 MB, atau 20,5 MB? Jawabannya ada di bagian "Empat penyebab yang paling sering".

Kapan memori dibersihkan

GC mulai dari akar: variabel global, window, DOM yang tampil di halaman, serta listener dan timer yang masih aktif. Semua yang bisa dijangkau dari akar itu dianggap masih dipakai. Yang gak bisa dijangkau dibuang. Kamu gak perlu (dan gak bisa) memanggil GC sendiri.

Jadi memory leak adalah memori yang sudah gak dipakai tapi gak bisa dibebaskan GC, karena masih ada referensi ke sana. Gejalanya: halaman yang awalnya lancar makin lambat setelah lama dipakai, dan tab-nya bisa crash. Di DevTools, grafik heap yang sehat naik-turun. Yang bocor naik terus seperti tangga.

Empat penyebab yang paling sering

Ini modal di Lab halaman ini. Tiap kali dibuka, dia membuat elemen modal (sekitar 0,5 MB) dan array produk (sekitar 2 MB).

const infoModal = new WeakMap(); // Map biasa bikin modal gak pernah dibuang

function bukaModal() {
  const modal = document.createElement("div");
  const produk = new Array(250000).fill("produk"); // sekitar 2 MB
  const controller = new AbortController();

  window.addEventListener("resize", () => posisikan(modal, produk), {
    signal: controller.signal,
  });
  const id = setInterval(() => segarkan(modal, produk), 1000);
  infoModal.set(modal, { dibukaPada: Date.now() });
  document.body.append(modal);

  return function tutupModal() {
    modal.remove();
    controller.abort(); // lepas listener-nya
    clearInterval(id); // hentikan interval-nya
  };
}

Setiap baris di tutupModal menutup satu jalur bocor:

  1. Listener yang gak dilepas. window hidup selama halaman dibuka. Dia memegang listener, listener-nya closure yang memegang modal dan produk. Tanpa controller.abort(), 2,5 MB itu tertahan.
  2. Timer yang gak dihentikan. setInterval jalan terus tiap detik sampai di-clearInterval. Selama itu callback dan closure-nya tetap hidup, beserta modal dan produk.
  3. Detached DOM. modal.remove() mengeluarkan elemen dari halaman, tapi kalau masih ada referensi dari JavaScript (misalnya jadi key di Map biasa), elemennya tetap di memori. Elemen seperti ini disebut detached DOM node.
  4. Closure yang memegang data besar. Ini yang bikin dua penyebab pertama mahal: closure-nya cuma butuh produk untuk segarkan, tapi selama closure-nya hidup, 2 MB itu ikut hidup.

Satu jalur saja sudah cukup untuk menahan semuanya. Di tebakan tadi, listener-nya sudah dilepas, tapi interval-nya masih memegang closure, jadi tiap buka-tutup tertahan 2,5 MB. Lima kali jadi 12,5 MB, dan heap naik dari 8 MB ke 20,5 MB. Itu jawaban tebakannya.

Bersihkan saat ditutup

Patokannya: apa pun yang dipasang saat membuka, dilepas saat menutup.

WeakMap untuk data tentang elemen

Map biasa memegang key-nya dengan kuat: selama entry-nya ada, elemen yang jadi key gak bisa dibuang, walaupun sudah lepas dari halaman. WeakMap memegang key-nya dengan lemah: kalau gak ada referensi lain ke elemen itu, entry-nya ikut hilang saat GC jalan. Pakai WeakMap (atau WeakSet) untuk menyimpan data tambahan tentang elemen DOM atau object lain yang seharusnya ikut dibuang. Gantinya, WeakMap gak bisa di-loop dan gak punya .size.

WeakRef mirip, untuk satu object: ref.deref() mengembalikan object-nya, atau undefined kalau sudah dibuang GC. Jarang dibutuhkan di kode sehari-hari.

Menemukan leak di DevTools

  1. Buka DevTools, tab Memory, pilih Heap snapshot, lalu klik Take snapshot.
  2. Lakukan aksi yang dicurigai, misalnya buka-tutup modal 5 kali.
  3. Klik ikon tong sampah (Collect garbage) supaya yang bisa dibuang benar-benar dibuang, lalu ambil snapshot kedua.
  4. Di snapshot kedua, pilih tampilan Comparison. Object yang bertambah tapi seharusnya sudah hilang adalah tersangka. Ketik Detached di kotak filter untuk mencari detached DOM node, lalu buka panel Retainers untuk melihat siapa yang masih memegangnya.

Mengecek pola leak dari teks kode

Latihan pelajaran ini memeriksa potongan kode sebagai teks, dengan aturan sederhana:

function findLeaks(codeSnippets) {
  const leaks = [];
  codeSnippets.forEach((snippet, index) => {
    if (snippet.includes("setInterval") && !snippet.includes("clearInterval")) {
      leaks.push({ snippet: index, leak: "interval-not-cleared" });
    }
    if (snippet.includes("addEventListener") && !snippet.includes("removeEventListener")) {
      leaks.push({ snippet: index, leak: "listener-not-removed" });
    }
  });
  return leaks;
}
Panggilan Hasil
findLeaks(["setInterval(() => update(), 1000)"]) [{ snippet: 0, leak: "interval-not-cleared" }]
findLeaks(["const t = setInterval(() => {}, 1000); clearInterval(t)"]) []
findLeaks(['document.addEventListener("click", fn)']) [{ snippet: 0, leak: "listener-not-removed" }]

⚠️ Jebakan yang sering ketemu

TL;DR: Memory leak adalah memori yang sudah gak dipakai tapi gak bisa dibebaskan GC, karena masih ada referensi ke sana. Penyebab paling sering: listener yang gak dilepas, setInterval tanpa clearInterval, detached DOM node yang masih disimpan di JavaScript, dan closure yang memegang data besar. Satu jalur saja cukup: modal 2,5 MB yang dibuka-tutup 5 kali dengan interval yang lupa dihentikan bikin heap naik dari 8 MB ke 20,5 MB. Lepas semuanya saat menutup: clearInterval(id), removeEventListener dengan function yang sama, atau satu controller.abort() untuk banyak listener. Simpan data tentang elemen di WeakMap. Cari leak dengan dua heap snapshot di tab Memory dan tampilan Comparison.

Yang akan kamu pelajari