How would you debug and fix a memory leak in a React application?
1) Reproduce — repeatedly trigger the suspected leaky path. 2) DevTools Memory panel — take heap snapshot, repeat the leak path, take another, compare; look at retained size + 'Detached HTMLElement' rows. 3) Identify the retainer — usually a missing listener removal, interval not cleared, or closure over big data. 4) Fix with cleanup in useEffect return / AbortController / bounded cache. 5) Verify heap returns to baseline.
Cross-link to [[how-do-you-handle-memory-leaks-in-react-apps]] for prevention. This question is about debugging an existing leak.
Step 1 — reproduce
Define the leak scenario:
"Open the modal, close it, repeat 50 times. Heap grows linearly."
Without a repro, you can't measure.
Step 2 — snapshot before/after
DevTools → Memory → "Heap snapshot."
- Snapshot at baseline.
- Do the suspected leaky action N times (e.g. open/close modal 50x).
- Snapshot again.
- Compare:
Comparisonmode shows what's "new" since the first snapshot.
Step 3 — find the retainer
In the comparison view:
- Look at "Detached HTMLElement" — DOM nodes still in JS memory after removal.
- Sort by retained size — what's holding the memory.
- Click a leaking object → "Retainers" tab shows what's referencing it.
The path from a GC root to the leaked object tells you who's holding it.
Common React leaks + fixes
Listener not removed
// LEAK
useEffect(() => {
window.addEventListener('resize', onResize);
}, []);
// FIX
useEffect(() => {
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);Or use AbortController for many at once:
useEffect(() => {
const ac = new AbortController();
window.addEventListener('resize', onResize, { signal: ac.signal });
document.addEventListener('keydown', onKey, { signal: ac.signal });
return () => ac.abort();
}, []);Interval / timeout not cleared
useEffect(() => {
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []);In-flight fetch after unmount
useEffect(() => {
const ac = new AbortController();
fetch(url, { signal: ac.signal }).then(setData).catch((e) => { if (e.name !== 'AbortError') ... });
return () => ac.abort();
}, []);Store subscription not unsubscribed
useEffect(() => {
const unsub = store.subscribe(onChange);
return unsub;
}, []);Closure over a big object
// LEAK
useEffect(() => {
const giantData = computeGiant();
setTimeout(() => use(giantData.summary), 60_000); // keeps giantData alive 60s
}, []);
// FIX
useEffect(() => {
const giantData = computeGiant();
const summary = giantData.summary;
setTimeout(() => use(summary), 60_000); // only summary kept
}, []);Unbounded cache
const cache = new Map(); // grows foreverBound with LRU + cap or TTL. Use WeakMap if keys are objects that can be GC'd.
Detached DOM held by JS
const ref = useRef();
useEffect(() => {
window.someGlobal = () => use(ref.current); // global retains the node
}, []);Avoid retaining DOM via globals.
Step 4 — Strict Mode in dev
React 18 Strict Mode double-mounts effects. If your cleanup is missing, the second mount registers a duplicate subscription. Run with Strict Mode to surface bugs early.
Step 5 — verify
Repeat the leak scenario; snapshot. Heap should return to ~baseline after GC. Force GC in DevTools or wait.
Production detection
performance.memory.usedJSHeapSize(Chrome) — sample periodically.- Sentry / RUM tools that flag heap growth.
- User reports of slow tabs after long sessions.
Interview framing
"Reproduce the leak path repeatably. Heap snapshot before and after; compare in DevTools Memory panel. Look at 'Detached HTMLElement' and retainers — they tell you what's holding the memory. Most React leaks are: missing useEffect cleanup (listener, interval, subscription, fetch), closures holding big data, unbounded caches, or DOM refs retained via globals. Fix with cleanup in useEffect return (or AbortController for many at once), bounded caches, WeakMap for object keys. Re-snapshot to verify return to baseline. Strict Mode in dev surfaces missing cleanups by double-mounting."
Follow-up questions
- •Walk me through finding a leak with DevTools.
- •Why does Strict Mode help?
- •When is WeakMap the right choice?
Common mistakes
- •Missing useEffect cleanup.
- •Global retention of refs.
- •Unbounded caches.
Performance considerations
- •Leak detection workflow + Strict Mode + AbortController convention prevents most issues.
Edge cases
- •WebSocket reconnect leaks.
- •Workers not terminated.
- •Cycles between React state and external library.
Real-world examples
- •Long-running SPAs (Slack, Gmail), dashboard apps, editor apps.