"매 SPA 가 시간이 지날수록 느려진다면 90% memory leak". JS 의 GC 는 매 reachable object 만 살리지만, detached DOM, forgotten timer, closure capture, global accumulation 이 매 unintended retention 을 만든다. 매 Chrome DevTools Memory panel + Performance panel 의 heap snapshot diff + allocation timeline 이 매 standard tool.
매 핵심
매 4 가지 흔한 leak pattern
Detached DOM: 매 element 를 removeChild 했지만 JS reference 가 살아있음 → DOM tree + element subtree 통째로 retained.
Forgotten timer / listener: setInterval / addEventListener 의 callback closure 가 매 component scope 를 hold.
Closure over big data: outer function 의 large array 를 inner closure 가 무의식적으로 capture.
Global accumulation: window.cache[key] = ... 식의 무한 적재.
매 detection workflow (3 snapshot technique)
App load → snapshot A.
매 leak-suspect action 100 회 (open/close modal …) → snapshot B.
매 동일 action 100 회 더 → snapshot C.
Comparison: B → C 사이 Delta 가 0 이어야 정상. 매 keep growing → leak.
매 응용
SPA debugging (React/Vue route transition leak).
Memory regression CI (puppeteer + heap snapshot).
Production monitoring (performance.memory).
💻 패턴
Detect detached DOM (DevTools console)
// 매 Memory panel → "Detached" filter 로 search
// 또는 console 에서:
functionfindDetached(){constall=$$('*');// visible
// queryObjects(HTMLElement) — DevTools console 전용
console.log('visible:',all.length);}queryObjects(HTMLElement);// 매 DevTools console only
importpuppeteerfrom'puppeteer';constbrowser=awaitpuppeteer.launch();constpage=awaitbrowser.newPage();awaitpage.goto('http://localhost:3000');constsession=awaitpage.target().createCDPSession();asyncfunctionsnapshot(){awaitsession.send('HeapProfiler.collectGarbage');constm=awaitpage.evaluate(()=>performance.memory.usedJSHeapSize);returnm;}consta=awaitsnapshot();for(leti=0;i<100;i++)awaitpage.click('#open-modal'),awaitpage.click('#close-modal');constb=awaitsnapshot();for(leti=0;i<100;i++)awaitpage.click('#open-modal'),awaitpage.click('#close-modal');constc=awaitsnapshot();constgrowth=c-b;if(growth>1_000_000)thrownewError(`Memory leak: +${growth} bytes between B→C`);
AbortController for fetch / event cleanup
constac=newAbortController();window.addEventListener('scroll',onScroll,{signal:ac.signal});fetch(url,{signal:ac.signal});// component unmount:
ac.abort();// 매 listener + fetch 동시 cleanup
Common React leak — stale closure capture
// 매 BAD — bigData 를 closure 가 잡음, unmount 후에도 retain
useEffect(()=>{constbigData=computeMassive();constid=setTimeout(()=>console.log(bigData[0]),60_000);// cleanup 없음 → leak
},[]);// 매 GOOD
useEffect(()=>{constbigData=computeMassive();constid=setTimeout(()=>console.log(bigData[0]),60_000);return()=>clearTimeout(id);},[]);