How do you detect a memory leak in a React or frontend application?
Chrome DevTools Memory tab: take a heap snapshot, navigate away from the suspect feature, force GC, take another snapshot. Compare. Filter by 'Detached HTMLElement' or by component class names. Performance Monitor shows growing heap or DOM node count over time. Common culprits: uncleaned event listeners, timers, subscriptions, refs holding unmounted DOM, closures capturing large state. StrictMode dev double-mount surfaces missing cleanups.
Methodical workflow — measure, isolate, fix.
Symptoms
- Browser tab gets slower over time, eventually crashes.
- Heap snapshot keeps growing even after the user navigates around.
- DOM node count increases after each navigation cycle.
- Performance Monitor shows allocations without corresponding GC drops.
DevTools workflow
1. Performance Monitor (live view)
Open DevTools → ... menu → More tools → Performance monitor. Watch JS heap size and DOM node count while you exercise the feature. Steady growth across cycles = leak.
2. Heap snapshots (precise)
DevTools → Memory tab → Heap snapshot
1. Take snapshot A
2. Navigate to the suspect feature, then back
3. Force GC (trash can icon)
4. Take snapshot B
5. Compare → 'Objects allocated between A and B'Look for:
- Detached HTMLElement — DOM nodes no longer in the tree but still referenced.
- Component instance names appearing in growing counts.
- Closures with unexpectedly large captured scopes.
3. Allocation timeline
DevTools → Memory → 'Allocation instrumentation on timeline' shows allocation rate over time. Spikes that don't free are leaks.
Common culprits in React
1. Event listeners.
useEffect(() => {
const onResize = () => setSize(window.innerWidth);
window.addEventListener('resize', onResize);
// BUG: no cleanup
}, []);Fix: return the cleanup.
return () => window.removeEventListener('resize', onResize);2. Timers / intervals.
useEffect(() => {
setInterval(tick, 1000); // BUG: never cleared
}, []);Fix: const id = setInterval(...); return () => clearInterval(id);
3. Subscriptions.
useEffect(() => {
const sub = stream.subscribe(handleData);
return () => sub.unsubscribe();
}, []);4. In-flight fetches that setState after unmount.
useEffect(() => {
const ctrl = new AbortController();
fetch(url, { signal: ctrl.signal }).then(r => setData(r));
return () => ctrl.abort();
}, [url]);5. Refs / global maps holding DOM.
const registry = new Map();
useEffect(() => {
registry.set(id, ref.current); // never cleaned up
}, []);Use WeakMap when you can't be sure of cleanup timing.
6. Closures capturing large state.
A long-lived handler (registered in a parent) that captures large data from a child can keep the child alive after unmount.
StrictMode helps
React 18 StrictMode in dev:
- Mounts component
- Unmounts immediately
- Mounts again
Any effect that doesn't clean up correctly will behave wrong on the second mount. The bug surfaces in dev where it's cheap to fix.
Runtime guards
Some teams add cleanup audits:
useEffect(() => {
let alive = true;
doAsync().then(d => { if (alive) setData(d); });
return () => { alive = false; };
}, []);Profiling production
- Sentry / Rollbar performance: real user monitoring shows session durations and crash rates.
- Custom telemetry: log heap size periodically (
performance.memory.usedJSHeapSizein Chrome). - Long sessions: dashboards left open for hours surface slow leaks that short tests miss.
Senior framing
Memory leaks in modern React are almost always missing cleanups in useEffect. The fix is mechanical once you spot the pattern. The hard part is detection — heap snapshots and Performance Monitor are the right tools. StrictMode catches a chunk of these in dev before they ship.
Follow-up questions
- •What does a 'detached HTMLElement' actually mean?
- •How does WeakMap help with reference-based leaks?
- •Why does StrictMode help find leaks early?
Common mistakes
- •Forgetting cleanup return from useEffect.
- •Calling setState after unmount — produces a warning AND can leak.
- •Adding listeners to window/document without removing on unmount.
Performance considerations
- •Memory leaks degrade performance over time. GC pauses get longer as the heap grows. Eventually the tab crashes or freezes. For long-lived apps (dashboards, chat) this is critical; for sessions under a minute, it rarely matters.
Edge cases
- •Service Workers can hold references that survive across navigations.
- •Memoized callbacks with large captured scopes can pin big objects in memory.
- •Browser extensions can hold references to your DOM and confuse leak hunting.
Real-world examples
- •Dashboards left open for 8 hours leak across navigations. Chat apps leak through unclosed event sources. SPAs accumulate listeners across route changes. RxJS without unsubscribe is a classic source.