Topik wawancara representatif

Wawancara Frontend: Bagaimana Cara Mendiagnosis dan Memperbaiki Kebocoran Memori (Memory Leak) JavaScript?

FrontendSulit
Tim Redaksi Offer.ccDipublikasikan Diperbarui

Pertanyaan

Sebuah dashboard pesanan single-page React membuat chart, berlangganan pesan, dan mengamati perubahan window setiap kali rute detailnya dibuka. Setelah 20 kali navigasi bolak-balik, halaman menjadi terasa lambat; setelah garbage collection dipaksa, jumlah JS heap, DOM node, dan listener masih terus meningkat di setiap putaran. Jelaskan bagaimana Anda membuktikan memory leak, menemukan retaining path-nya dengan Chrome DevTools, memperbaiki siklus hidup resource, dan merancang acceptance test yang dapat direproduksi.

Konteks dan Permintaan

Sebuah dashboard pesanan single-page React berisi grafik live, langganan event pesanan, dan perilaku tata letak responsif. Pengguna membuka rute detail pesanan, kembali ke daftar, dan mengulangi navigasi tersebut sebanyak 20 kali. Halaman kemudian menjadi lambat. Rekaman Chrome Performance menunjukkan bahwa setelah operasi yang sama dan adanya kesempatan garbage collection di akhir setiap putaran, batas bawah (low-water mark) JS heap terus meningkat. Jumlah DOM node dan listener juga gagal kembali ke rentang awalnya.

Komponen yang relevan disederhanakan di bawah ini:

tsx
useEffect(() => {
  const chart = createOrderChart(containerRef.current)

  window.addEventListener("resize", () => chart.resize())
  orderBus.on("order", (order) => chart.append(order))
  const timerId = window.setInterval(() => chart.refresh(), 5_000)

  return () => {
    window.clearInterval(timerId)
  }
}, [])

Wawancara ini meminta rantai bukti yang dapat ditinjau (reviewable evidence chain). Kandidat harus memisahkan alokasi biasa, memory bloat, garbage collection yang sering, dan kebocoran nyata; mengidentifikasi apa yang menahan objek yang seharusnya sudah mati dari GC root; memperbaiki masa pakai listener, langganan, timer, dan instans pihak ketiga; serta membuktikan perbaikannya dengan loop aksi yang sama.

Bank soal frontend publik tahun 2025 secara eksplisit menanyakan cara mengidentifikasi dan memperbaiki kebocoran memori JavaScript. Panduan wawancara JavaScript 2025 lainnya menggunakan SPA yang melambat setelah navigasi berulang dan menanyakan bagaimana DevTools menemukan kebocoran tersebut. Akun wawancara publik baru-baru ini mencatat pertanyaan lanjutan seputar garbage collection, closure, listener, dan unmounting komponen. Tujuan pencariannya konkret: kandidat memerlukan alur kerja diagnostik browser yang dapat dieksekusi daripada sekadar daftar yang berakhir pada "bersihkan timer".

Apa yang Dinilai oleh Pewawancara

Keahlian pertama adalah menetapkan aturan keputusan yang valid. Peningkatan objek sementara, lonjakan puncak yang lebih tinggi, atau angka memori proses yang meningkat tidak membuktikan adanya kebocoran dengan sendirinya. Jawaban yang kuat menetapkan browser, build, data, dan urutan tindakan yang tetap; memberi GC kesempatan yang sebanding di antara putaran; dan membandingkan beberapa baseline aktif. Alokasi biasa dapat membentuk pola gigi gergaji (sawtooth) yang naik dan turun. Objek yang bertahan dan terakumulasi setelah operasi yang sama membenarkan penyelidikan jalur referensinya.

Keahlian kedua adalah keterjangkauan (reachability). Garbage collector JavaScript modern menandai objek yang dapat dijangkau dari root seperti objek global. Aplikasi yang tidak lagi menginginkan suatu objek tidak membuat fakta itu terlihat oleh collector. Jika listener window, event bus, timer, cache, atau library pihak ketiga masih memegang referensi kuat, objek tersebut tetap dapat dijangkau. Siklus (cycle) saja belum tentu bocor: seluruh kelompok siklis dapat dikumpulkan setelah tidak ada lagi jalur dari root yang mencapainya.

Keahlian ketiga adalah membaca bukti alih-alih hanya membuka panel Memory:

BuktiPertanyaan yang dijawabApa yang tidak dapat dibuktikannya sendiri
Kurva memori Task Manager / PerformanceApakah pekerjaan yang berulang terus meningkatkan memori?Objek mana yang menahannya (retains)
Heap Snapshot ComparisonTipe aktif dan jumlah referensi mana yang meningkat?Apakah pertumbuhan memenuhi kontrak cache
Retainers / retaining pathRantai referensi mana yang menghubungkan objek ke root?Kapan kode pemilik harus melepaskannya
Allocation instrumentation on timelineTindakan mana yang mengalokasikan objek yang kemudian bertahan?Bahwa setiap alokasi adalah kebocoran
Jumlah DOM node, dokumen, dan listenerApakah pertumbuhan berorientasi pada DOM atau listener?Memori proses lengkap di luar JS heap

Keahlian terakhir adalah kepemilikan resource (resource ownership). Batasan siklus hidup yang membuat resource harus melepaskannya. AbortController dapat menghapus DOM listener yang terdaftar dengan signal miliknya, tetapi tidak memutuskan ResizeObserver, menutup WebSocket, berhenti berlangganan dari event bus, atau menghancurkan chart pihak ketiga. Resource tersebut masih memerlukan operasi disconnect, close, unsubscribe, atau destroy mereka sendiri.

Pertanyaan untuk Diklarifikasi Sebelum Menjawab

  • Apa sebenarnya yang mereproduksi masalah tersebut? Tetapkan titik masuk, aksi kembali, kondisi kesiapan per putaran, volume data, dan jumlah pengulangan. Pesanan yang berbeda di setiap putaran mencemari perbedaan snapshot dengan perbedaan data bisnis.
  • Pengukuran mana yang meningkat? Jejak memori OS, JS heap, DOM node, dokumen, dan listener mencakup wilayah yang berbeda. Persempit masalah sebelum memilih analisis snapshot atau alokasi.
  • Apakah produk sengaja melakukan caching terhadap sesuatu? Cache rute berbatas atau riwayat grafik mungkin tetap aktif sesuai desain. Kapasitas, penggusuran (eviction), dan ekspektasi kondisi stabil (steady-state) membedakan penggunaan terkontrol dari kebocoran.
  • Apakah komponen benar-benar di-unmount? Perutean (routing) dapat menyembunyikan, mempertahankan, atau menggunakannya kembali. Konfirmasikan siklus hidup sebenarnya dengan log mount dan cleanup sebelum memperbaiki unmount yang tidak pernah terjadi.
  • Objek mana yang berasal dari library pihak ketiga? Grafik, editor, dan peta sering kali memiliki DOM, worker, observer, dan listener internal sendiri. Menghapus HTML container tidak menghancurkan instansnya.
  • Bisakah gejala produksi direproduksi di lingkungan pengujian? Heap snapshot mungkin berisi data pengguna. Lebih baik lakukan reproduksi terkontrol dengan data yang disanitasi dan terapkan kontrol privasi serta akses.
  • Apa tolok ukur keberhasilannya? Tidak ada batas MB universal yang independen dari perangkat dan beban kerja. Sepakati kemiringan baseline pasca-operasi, jumlah instans aktif, jumlah node/listener, dan hambatan (stalls) yang terlihat oleh pengguna.

Kerangka Jawaban 30 Detik

"Saya akan menetapkan tindakan daftar-detail-daftar, melakukan pemanasan (warm-up), mengulanginya, dan membandingkan baseline aktif pada checkpoint GC yang identik. Jika titik rendah heap, DOM node, atau listener terakumulasi, saya membandingkan snapshot, menemukan tipe yang bertambah, dan menelusuri Retainers ke window, event bus, atau timer. Saya menambahkan Allocation timeline jika sumbernya tidak jelas. Pembuatnya kemudian melepaskan listener, langganan, timer, observer, dan chart. Saya menjalankan kembali loop yang sama dan memastikan jumlah instans turun, baseline stabil, dan perilaku tetap benar."

Penyelidikan Mendalam Langkah Demi Langkah

Langkah 1: Buktikan kebocoran dengan eksperimen berulang

Mulailah dengan baseline yang sebanding. Gunakan versi Chrome yang sama dengan ekstensi yang tidak terkait diminimalkan, dan tetapkan window, akun uji, data pesanan, dan rute. Setelah memuat ulang, selesaikan satu kali pemanasan agar modul lazy, font, pool koneksi, dan cache satu kali terinisialisasi. Di panel Performance, aktifkan Memory dan jalankan:

  1. Picu garbage collection satu kali dan catat titik awal.
  2. Selesaikan lima perjalanan daftar-detail-daftar, menunggu kondisi "chart rendered" yang sama setiap kali.
  3. Picu garbage collection lagi dan catat titik rendah JS heap, DOM node, dokumen, dan listener.
  4. Ulangi tiga grup dan bandingkan titik rendah antar grup daripada puncak arbitrer.

Waktu pengumpulan memori bergantung pada runtime. Eksperimen singkat juga dipengaruhi oleh kerja JIT, decoding gambar, respons jaringan, dan DevTools itu sendiri, sehingga selisih satu putaran hanya menciptakan hipotesis. Grup pertama yang lebih tinggi dan kemudian stabil bisa jadi hanya pemanasan. Jika setiap grup mempertahankan satu OrderChart baru, sekumpulan node yang terlepas (detached), dan dua listener secara proporsional dengan jumlah tindakan, buktinya menjadi lebih kuat.

Pisahkan tiga fenomena:

  • Kebocoran (Leak): baseline aktif pasca-GC terus meningkat setelah operasi yang sama.
  • Pembengkakan memori (memory bloat): penggunaan dalam kondisi stabil berlebihan tetapi tidak lagi bertambah tanpa batas seiring jumlah operasi.
  • Pergantian alokasi berlebih (allocation churn): heap naik dan turun dengan jeda GC yang sering, dan titik rendah pulih; masalahnya adalah terlalu banyak alokasi sementara.

Memory footprint di Task Manager mencakup penggunaan proses seperti penyimpanan DOM. Nilai aktif di JavaScript Memory lebih dekat ke JS heap yang dapat dijangkau. Pengukuran ini memilah penyelidikan, tetapi tidak menggantikan bukti referensi dari heap snapshot. Forced GC adalah kontrol diagnostik, bukan perbaikan produk, dan kode produksi tidak dapat menjanjikan pengumpulan pada momen tertentu.

Langkah 2: Temukan pemilik yang bertanggung jawab dengan snapshot dan retaining path

Setelah memastikan adanya pertumbuhan, ambil Snapshot A di panel Memory. Lakukan loop navigasi yang telah ditentukan, kembali ke daftar, tunggu pembersihan asinkron, dan ambil Snapshot B. Pengambilan snapshot dimulai dengan garbage collection, jadi Comparison berguna untuk objek yang tetap dapat dijangkau. Urutkan berdasarkan delta instans dan retained size, cari konstruktor bisnis, closure, array, dan DOM tree terlepas yang bertambah seiring jumlah loop.

shallow size mendeskripsikan objek itu sendiri. retained size memperkirakan memori yang dapat dilepaskan jika objek tersebut menjadi tidak dapat dijangkau. Callback listener kecil dapat mempertahankan chart, array data, dan seluruh subtree DOM melalui closure-nya, sehingga retained size adalah sinyal prioritas yang lebih baik daripada ukuran callback. Ini adalah perkiraan penyelidikan, bukan klaim langsung atas memori fisik eksklusif.

Pilih OrderChart atau node terlepas yang seharusnya sudah mati dan periksa Retainers. Jalurnya mungkin terlihat seperti:

text
Window
└─ resize event listener
   └─ callback closure
      └─ chart
         └─ container
            └─ detached HTMLDivElement

Jalur ini menjelaskan mengapa GC tidak dapat mengumpulkan grafik tersebut: window global masih memiliki anonymous callback resize, yang closure-nya memiliki chart. Menghapus DOM dari dokumen mengubah hubungannya dengan dokumen; itu tidak memutuskan jalur JavaScript dari root. Perbaikan harus dilakukan pada kepemilikan listener dan chart, bukan pada penetapan spekulatif null ke node yang terlepas.

Jika snapshot hanya menampilkan entri generik Object dan Array, gunakan Allocation instrumentation on timeline. Mulai merekam, lakukan tepat satu tindakan yang bocor, hentikan, dan fokus pada alokasi yang tetap hidup setelah pengumpulan. Telusuri konstruktor dan allocation stack-nya kembali ke kode. Allocation sampling memiliki overhead lebih rendah dan berguna untuk menemukan fungsi yang sering mengalokasikan memori, tetapi hasil samplingnya tidak menggantikan snapshot ketika instans individual dan retainers menjadi hal yang penting.

Sumber penahanan umum meliputi:

  • objek global atau array modul yang menambahkan instans halaman tanpa batas;
  • listener DOM/EventTarget yang tidak pernah dihapus, atau dihapus dengan identitas fungsi atau opsi capture yang berbeda;
  • callback setInterval dan timeout rekursif yang mempertahankan state komponen;
  • event bus, store, WebSocket, atau observable tanpa berhenti berlangganan (unsubscription);
  • ResizeObserver, IntersectionObserver, worker, dan instans pihak ketiga tanpa destruksi;
  • Map, koleksi riwayat, atau cache permintaan tanpa batas kapasitas;
  • promise yang tidak pernah selesai atau antrean pekerjaan yang mempertahankan closure untuk waktu yang tidak terbatas.

Langkah 3: Perbaiki kepemilikan resource dan jalankan kembali acceptance test yang sama

Effect yang diperbaiki mempertahankan handle pelepasan untuk setiap resource. AbortController dapat memiliki listener DOM, sementara resource lainnya dilepaskan secara eksplisit:

tsx
useEffect(() => {
  const container = containerRef.current
  if (!container) return

  const controller = new AbortController()
  const chart = createOrderChart(container)
  const observer = new ResizeObserver(() => chart.resize())
  const unsubscribe = orderBus.on("order", (order) => chart.append(order))
  const timerId = window.setInterval(() => chart.refresh(), 5_000)

  observer.observe(container)
  window.addEventListener("visibilitychange", () => chart.syncVisibility(), {
    signal: controller.signal,
  })

  return () => {
    controller.abort()
    observer.disconnect()
    unsubscribe()
    window.clearInterval(timerId)
    chart.destroy()
  }
}, [])

Verifikasi kontrak API yang sebenarnya. Beberapa metode on mengembalikan fungsi unsubscribe; yang lain mengharuskan handler yang sama diberikan ke off. Jika dependensi yang berubah dapat membuat ulang resource, pembersihan harus melepaskan hanya instans yang dibuat oleh eksekusi Effect tersebut dan tidak boleh menutup penerusnya. Permintaan asinkron juga memerlukan pembatalan atau pemeriksaan generasi. Mencegah penulisan state setelah unmount hanya mengatasi satu efek samping; kebocoran tetap ada jika langganan eksternal masih memiliki closure tersebut.

WeakMap sesuai untuk metadata dengan kunci objek, tetapi tidak menggantikan pembersihan siklus hidup eksplisit. WeakRef sengaja memberikan sedikit jaminan tentang waktu pengumpulan dan perilaku yang dapat diamati, dan biasanya merupakan perbaikan yang buruk untuk listener, koneksi, atau instans chart. Perbaikan langsungnya adalah memutuskan referensi kuat yang tidak diperlukan dan memberikan kapasitas, TTL, atau kondisi penggusuran pada cache yang disengaja.

Acceptance test menjalankan kembali eksperimen yang sama persis sebelum perbaikan: build, data, jumlah navigasi, titik kesiapan, dan kontrol GC yang sama. Kondisi kelulusan meliputi:

  • komponen detail aktif, instans chart, dan subtree yang terlepas kembali ke jumlah yang disepakati setelah ditinggalkan;
  • di beberapa grup, baseline pasca-GC berfluktuasi dalam rentang yang stabil alih-alih mengikuti jumlah navigasi secara linier;
  • jumlah listener, dokumen, dan DOM node kembali ke rentang yang diharapkan;
  • setiap mount memiliki satu langganan pesan dan satu instans chart, dan setiap unmount melepaskan masing-masing tepat satu kali;
  • grafik tetap diperbarui, pengubahan ukuran container dan perilaku visibilitas berfungsi, dan kunjungan ulang membuat ulang resource;
  • hambatan yang berlangsung lama, jeda GC, dan sinyal crash memenuhi anggaran produk.

Simpan snapshot sebelum dan sesudah, langkah reproduksi, identitas build, dan jalur penahanan utama untuk peninjauan. Snapshot dapat berisi string dan objek bisnis, jadi simpan dan bagikan sebagai materi debugging yang sensitif.

Contoh Jawaban Berkualitas Tinggi

"Kurva tersebut adalah bukti yang kuat, tetapi satu kenaikan belum cukup untuk menarik kesimpulan saya. Saya akan menetapkan data pesanan, mendefinisikan daftar-detail-daftar sebagai satu operasi, melakukan pemanasan, lalu menjalankan tiga grup yang masing-masing terdiri dari lima putaran. Pada status halaman yang identik sebelum dan sesudah setiap grup, saya akan meminta GC dan mencatat titik rendah heap, DOM node, dokumen, dan listener. Jika hanya grup pertama yang naik dan grup berikutnya stabil, saya memeriksa inisialisasi atau cache yang berbatas. Jika setiap grup menambahkan jumlah chart dan listener yang sama, saya melanjutkan ke snapshot."

"Saya mengambil Snapshot A setelah pemanasan, menjalankan loop, kembali ke daftar, menunggu pembersihan, dan mengambil Snapshot B. Di Comparison, saya mulai dengan delta instans dan retained size. Misalkan saya menemukan 15 instans OrderChart yang seharusnya mati, dengan Window → resize listener → closure → chart → detached container di Retainers. Jalur tersebut menjelaskan kegagalannya: window masih memiliki listener anonim, yang closure-nya mempertahankan chart dan DOM. Jika nama konstruktor bersifat generik, saya merekam satu Allocation timeline dan menggunakan allocation stack-nya untuk memetakan objek yang bertahan dari navigasi tersebut kembali ke kode."

"Untuk perbaikannya, Effect mempertahankan setiap handle pelepasan: listener DOM menggunakan AbortController, event bus dihentikan langganannya, timer dibersihkan, observer diputuskan, dan chart dihancurkan. Jika suatu library memerlukan off(handler), saya mempertahankan identitas handler yang sama. Cache diberi kapasitas atau penggusuran. Saya menjalankan kembali eksperimen yang identik dan meluluskannya hanya jika chart dan subtree yang terlepas kembali ke jumlah yang disepakati, baseline pasca-GC berhenti mengikuti jumlah navigasi, dan kunjungan ulang tetap berlangganan dan me-render."

Kesalahan Umum

  • Menyatakan kebocoran hanya dari satu puncak → Alokasi biasa, kerja JIT, dan cache juga menaikkan puncak → Bandingkan beberapa baseline pasca-GC dan korelasikan jumlah instans dengan jumlah operasi.
  • Membersihkan container setelah melihat DOM yang terlepas → Listener global atau closure mungkin masih menahannya → Ikuti Retainers ke root dan lepaskan pemilik sebenarnya.
  • Mengganti setiap struktur dengan WeakMap atau WeakRef → Referensi kuat lainnya masih membuat objek dapat dijangkau → Hapus listener, langganan, timer, dan referensi cache yang tidak diinginkan terlebih dahulu.
  • Hanya mencegah setState setelah unmount → Resource eksternal mungkin masih mempertahankan callback dan grafik objek → Batalkan pekerjaan dan hapus pendaftarannya.
  • Hanya memverifikasi bahwa halaman tetap dapat diklik → Kebenaran fungsional tidak membuktikan pelepasan memori → Ulangi eksperimen snapshot yang identik dan bandingkan instans aktif, baseline, dan retaining path.

Pertanyaan Lanjutan dan Tanggapan

Lanjutan 1: Mengapa referensi sirkular tidak selalu bocor?

Mark-and-sweep menanyakan apakah root dapat menjangkau objek tersebut. Dua objek dapat saling mereferensikan dan tetap dapat dikumpulkan ketika tidak ada jalur eksternal yang dapat dijangkau yang menunjuk ke kelompok tersebut. Jika listener window, cache modul, atau timer aktif menjangkau salah satunya, seluruh kelompok tetap dapat dijangkau.

Lanjutan 2: Mengapa DOM node yang dihapus dapat tetap berada di snapshot?

Penghapusan melepaskan node dari pohon dokumen. Variabel JavaScript, callback listener, komponen pihak ketiga, atau observer masih dapat mereferensikannya. Periksa Retainers dari node yang terlepas, temukan pemilik yang masih hidup di sepanjang jalur tersebut, dan panggil operasi pelepasan milik pemilik tersebut.

Lanjutan 3: Kapan Anda memilih Heap Snapshot, Allocation timeline, atau sampling?

Snapshot membandingkan objek aktif pada titik stabil dan mengekspos retaining path per instans. Allocation timeline menghubungkan tindakan pengguna dengan alokasi yang tetap hidup setelahnya. Sampling menemukan fungsi yang bertanggung jawab atas alokasi berat dengan overhead lebih rendah. Urutan praktisnya adalah membuktikan kurva terlebih dahulu, menggunakan snapshot untuk objek dan referensi, lalu menambahkan perekaman alokasi jika sumbernya masih belum jelas.

Lanjutan 4: Bisakah "memori harus bertambah kurang dari 10 MB" menjadi gerbang otomatis (automated gate)?

Nilai absolut yang tetap sensitif terhadap perangkat, browser, build, data, dan waktu GC. Gerbang yang lebih kuat menetapkan lingkungan dan tindakan, melakukan pemanasan, mengulangi grup, dan memeriksa kemiringan baseline pasca-GC, jumlah aktif dari konstruktor terkontrol, serta jumlah DOM/listener. Tetapkan ambang batas dari distribusi historis halaman tersebut dan anggaran produk, dengan toleransi kebisingan (noise) yang eksplisit.

Lanjutan 5: Bagaimana jika halaman disembunyikan tetapi tidak pernah di-unmount?

Klarifikasi kontrak produk. Untuk perilaku keep-alive yang disengaja, jeda timer, kurangi sampling, atau hentikan langganan yang tidak terlihat dan lanjutkan nanti; tetap batasi cache dan jumlah instans. "Nol setelah kembali ke daftar" tidak lagi berlaku. Acceptance harus menuntut penggunaan yang stabil dan terbatas serta pelepasan penuh saat komponen benar-benar dihancurkan.

Sumber publik

Pertanyaan terkait