"매 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.
WeakRef + FinalizationRegistry: 매 GC 관찰 (debugging only).
매 응용
SPA 매 route navigation 매 retain leak 진단.
Long-running dashboard 매 hour-scale leak 감시.
Node.js server 매 RSS growth 매 root cause.
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)
// Heap snapshot Class filter:
// "Detached HTMLDivElement"
// "Detached HTMLElement"
// 매 instance 매 retainer chain 매 inspect — 매 root retainer 매 leak 출처
Event listener leak — fix pattern
// 매 BAD
classWidget{constructor(){window.addEventListener('resize',this.onResize.bind(this));}onResize(){/* ... */}destroy(){/* listener still attached */}}// 매 GOOD
classWidget{constructor(){this.onResize=this.onResize.bind(this);window.addEventListener('resize',this.onResize);}onResize(){/* ... */}destroy(){window.removeEventListener('resize',this.onResize);}}
AbortController 매 modern cleanup
classComponent{constructor(){this.ac=newAbortController();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
// 매 BAD — closure captures large data
functionstartPolling(bigData){setInterval(()=>{console.log(bigData.length);// bigData retained forever
},1000);}// 매 GOOD — explicit handle + cleanup
consthandle=setInterval(poll,1000);functionstop(){clearInterval(handle);}
WeakMap 매 cache without leak
// 매 BAD — Map 매 key 매 GC X
constcache=newMap();functiongetMeta(node){if(!cache.has(node))cache.set(node,computeMeta(node));returncache.get(node);// node removed from DOM but still in cache
}// 매 GOOD — WeakMap key 매 GC 가능
constcache=newWeakMap();functiongetMeta(node){if(!cache.has(node))cache.set(node,computeMeta(node));returncache.get(node);}
performance.measureUserAgentSpecificMemory
// crossOriginIsolated context (COOP+COEP headers) 필요
if(crossOriginIsolated&&performance.measureUserAgentSpecificMemory){constresult=awaitperformance.measureUserAgentSpecificMemory();console.log('bytes:',result.bytes);console.table(result.breakdown);}
FinalizationRegistry 매 GC 관찰 (debug only)
constregistry=newFinalizationRegistry((tag)=>{console.log(`GC'd: ${tag}`);});classTracked{constructor(name){registry.register(this,name);}}newTracked('widget-1');// → "GC'd: widget-1" eventually (or never)
Node.js heap snapshot
node --inspect server.js
# 매 chrome://inspect → Memory → Take heap snapshot# 또는 programmatic:
언제: 매 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.