--- id: wiki-2026-0508-memory-leak-debugging-in-javascr title: Memory Leak Debugging in JavaScript category: 10_Wiki/Topics status: verified canonical_id: self aliases: [JS Memory Leak, Heap Leak Debugging] duplicate_of: none source_trust_level: A confidence_score: 0.9 verification_status: applied tags: [javascript, performance, debugging, memory] raw_sources: [] last_reinforced: 2026-05-10 github_commit: pending tech_stack: language: JavaScript framework: Chrome DevTools --- # Memory Leak Debugging in JavaScript ## 매 한 줄 > **"매 unintended retention — 매 GC 매 reach 가능한 reference chain 매 끊지 못해 매 heap 매 grows unbounded"**. JS 매 mark-and-sweep GC 자동이지만 매 closure/listener/global/timer 매 long-lived reference 매 object lifecycle 매 의도와 분리시키면 매 leak 발생, 매 Chrome DevTools Heap Snapshot 매 진단 standard. ## 매 핵심 ### 매 leak sources (top 5) - **Detached DOM nodes**: 매 element removed from tree 매 JS reference 잔존. - **Event listeners**: 매 addEventListener 매 removeEventListener 없이 매 component unmount. - **Timers**: setInterval/setTimeout 매 cleanup 누락 매 closure 매 모두 retain. - **Closures**: outer scope variables 매 inner function 매 capture 후 매 long-lived. - **Global accumulation**: window/globalThis 매 cache/array 매 unbounded push. ### 매 detection tools - **Chrome DevTools Memory**: Heap snapshot, allocation timeline, allocation sampling. - **performance.measureUserAgentSpecificMemory()** (Chrome 89+): 매 cross-origin isolated context. - **Node.js**: --inspect + Chrome DevTools, heapdump module, --heap-prof flag. - **WeakRef + FinalizationRegistry**: 매 GC 관찰 (debugging only). ### 매 응용 1. SPA 매 route navigation 매 retain leak 진단. 2. Long-running dashboard 매 hour-scale leak 감시. 3. Node.js server 매 RSS growth 매 root cause. 4. React/Vue component lifecycle leak detection. ## 💻 패턴 ### Heap snapshot 3-snapshot technique ``` 1. App 초기 load → Snapshot 1 (baseline) 2. Suspect action 수행 (modal open/close ×10) → Snapshot 2 3. 동일 action 재수행 → Snapshot 3 4. Snapshot 3 의 Comparison → Snapshot 1 5. "Allocated between snapshots 1 and 3" 의 still-alive objects = leak ``` ### Detached DOM 탐색 (DevTools Console) ```js // Heap snapshot Class filter: // "Detached HTMLDivElement" // "Detached HTMLElement" // 매 instance 매 retainer chain 매 inspect — 매 root retainer 매 leak 출처 ``` ### Event listener leak — fix pattern ```js // 매 BAD class Widget { constructor() { window.addEventListener('resize', this.onResize.bind(this)); } onResize() { /* ... */ } destroy() { /* listener still attached */ } } // 매 GOOD class Widget { constructor() { this.onResize = this.onResize.bind(this); window.addEventListener('resize', this.onResize); } onResize() { /* ... */ } destroy() { window.removeEventListener('resize', this.onResize); } } ``` ### AbortController 매 modern cleanup ```js class Component { constructor() { this.ac = new AbortController(); const { signal } = this.ac; window.addEventListener('scroll', this.onScroll, { signal }); window.addEventListener('resize', this.onResize, { signal }); fetch('/api', { signal }); } destroy() { this.ac.abort(); // 매 모든 listener + fetch 매 한 번에 cleanup } } ``` ### Timer leak fix ```js // 매 BAD — closure captures large data function startPolling(bigData) { setInterval(() => { console.log(bigData.length); // bigData retained forever }, 1000); } // 매 GOOD — explicit handle + cleanup const handle = setInterval(poll, 1000); function stop() { clearInterval(handle); } ``` ### WeakMap 매 cache without leak ```js // 매 BAD — Map 매 key 매 GC X const cache = new Map(); function getMeta(node) { if (!cache.has(node)) cache.set(node, computeMeta(node)); return cache.get(node); // node removed from DOM but still in cache } // 매 GOOD — WeakMap key 매 GC 가능 const cache = new WeakMap(); function getMeta(node) { if (!cache.has(node)) cache.set(node, computeMeta(node)); return cache.get(node); } ``` ### performance.measureUserAgentSpecificMemory ```js // crossOriginIsolated context (COOP+COEP headers) 필요 if (crossOriginIsolated && performance.measureUserAgentSpecificMemory) { const result = await performance.measureUserAgentSpecificMemory(); console.log('bytes:', result.bytes); console.table(result.breakdown); } ``` ### FinalizationRegistry 매 GC 관찰 (debug only) ```js const registry = new FinalizationRegistry((tag) => { console.log(`GC'd: ${tag}`); }); class Tracked { constructor(name) { registry.register(this, name); } } new Tracked('widget-1'); // → "GC'd: widget-1" eventually (or never) ``` ### Node.js heap snapshot ```bash node --inspect server.js # 매 chrome://inspect → Memory → Take heap snapshot # 또는 programmatic: ``` ```js import { writeHeapSnapshot } from 'node:v8'; const path = writeHeapSnapshot(); // .heapsnapshot file console.log(`Snapshot: ${path}`); ``` ## 매 결정 기준 | 상황 | Tool/Approach | |---|---| | Browser SPA growing memory | DevTools Heap Snapshot 3-snapshot | | 매 frame allocation hotspot | Allocation timeline (sampling) | | Detached DOM 의심 | Class filter "Detached " in snapshot | | Node.js RSS growth | writeHeapSnapshot + Chrome DevTools | | Continuous monitoring (production) | performance.measureUserAgentSpecificMemory | | Event listener leak | AbortController 매 unified cleanup | **기본값**: 매 Heap Snapshot 3-snapshot diff — 매 retainer chain 매 따라 root 매 식별. ## 🔗 Graph - 부모: [[Garbage-Collection]] - Adjacent: [[Chrome-DevTools]] · [[AbortController]] ## 🤖 LLM 활용 **언제**: 매 SPA/long-running app 의 메모리 증가, 매 unmount 후 referent 잔존, 매 production memory metrics 의 anomaly. **언제 X**: 매 short-lived script (CLI tool), 매 GC pause 문제 (different — GC tuning territory). ## ❌ 안티패턴 - **`delete` keyword 의존**: 매 reference 매 nullify 안 함 — 매 다른 reference 매 retain. - **`window.gc()` 매 production**: 매 only with --expose-gc flag, 매 hint 일 뿐. - **Allocation timeline 매 production trace**: 매 overhead 매 큼 — 매 staging 에서. - **One-snapshot 진단**: 매 baseline 없으면 매 noise 와 leak 매 구분 불가. - **DevTools 매 incognito 가정**: 매 extension 매 heap pollution — 매 incognito + 매 disabled extensions. ## 🧪 검증 / 중복 - Verified (Chrome DevTools docs, V8 blog, Node.js v8 module). - 신뢰도 A. ## 🕓 Changelog | 날짜 | 변경 | |---|---| | 2026-05-08 | Phase 1 | | 2026-05-10 | Manual cleanup — leak source taxonomy + DevTools workflow + AbortController/WeakMap patterns |