How does React handle re renders and memory management?
React re-renders by calling the component function again, building a new VDOM, diffing, and committing minimal DOM changes. Memory: each fiber persists across renders (state, refs, effect cleanups). Old VDOM nodes are discarded for GC. Memory leaks come from uncleaned subscriptions, timers, detached DOM references in refs, and closures that capture large objects. Effect cleanup is the primary defense.
Two related questions: how re-renders work, and what stays in memory between them.
How re-renders happen
- State setter is called → React enqueues an update on the owning fiber.
- React schedules a render (sync or via concurrent scheduler).
- The component function is called again — top to bottom.
- Hooks read their stored values from the fiber by call order.
- JSX is returned → new VDOM tree.
- React diffs against previous tree.
- Minimal DOM mutations are committed.
- Effects run after paint (useEffect) or before (useLayoutEffect).
What persists across renders
Per fiber (React's internal node for each component instance):
- useState values — stored on the hook slot.
- useRef.current — mutable, survives renders.
- useMemo / useCallback caches — until deps change.
- Effect cleanup functions — invoked before the next setup.
- DOM node references — attached via ref.
- Context subscriptions — fibers register interest.
What's collected
- Previous VDOM — plain objects, eligible for GC once the new tree commits.
- Discarded DOM nodes — removed from parent, GC'd if not referenced elsewhere.
- Unmounted fibers — released after cleanup runs.
Memory leak sources
1. Uncleaned subscriptions / timers.
useEffect(() => {
const id = setInterval(tick, 1000);
return () => clearInterval(id); // critical
}, []);2. Event listeners on window/document.
useEffect(() => {
const onResize = () => setW(window.innerWidth);
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);3. Detached DOM held by a ref.
A ref pointing to a DOM node that's been unmounted but the ref is held by a closure or external structure prevents GC.
4. setState after unmount (in-flight async).
useEffect(() => {
let alive = true;
fetch(url).then(r => r.json()).then(d => {
if (alive) setData(d);
});
return () => { alive = false; };
}, [url]);Better: AbortController.
useEffect(() => {
const ctrl = new AbortController();
fetch(url, { signal: ctrl.signal }).then(...);
return () => ctrl.abort();
}, [url]);5. Closures capturing big state.
Long-lived handlers that capture large props/state can keep them alive after the component unmounts.
StrictMode helps surface these
In dev, React 18 mounts → unmounts → mounts every component once. Cleanup that doesn't reverse setup fully will fail loudly.
Profiling
- Chrome DevTools → Memory → heap snapshot → look for 'Detached HTMLElement' or large component instance counts.
- Take a snapshot, navigate away, force GC, take another. Same instances? Leak.
- React DevTools Profiler shows commit duration but not memory.
Performance vs memory tradeoffs
- useMemo / useCallback caches add memory to save CPU.
- Memoized leaf components retain their old subtree until next render.
- React's fiber tree itself has overhead — typically ~1 KB per component instance.
For huge component trees, memory can be the bottleneck before CPU.
Follow-up questions
- •What's a 'detached DOM' leak and how do you find it?
- •Why does StrictMode dev double-mount help with memory bugs?
- •When does React garbage-collect old fibers?
Common mistakes
- •Forgetting cleanup on subscriptions / timers / listeners.
- •Calling setState after unmount — works but logs warnings and may leak.
- •Storing large objects in closures that outlive the component.
Performance considerations
- •Re-render cost = function execution + VDOM diff + reconciliation. Memory cost = fiber tree + cached hooks + closures. For most apps both are negligible; for very large trees (~10k nodes) both become real and need profiling.
Edge cases
- •Refs holding DOM nodes can prevent GC of subtrees after unmount.
- •Service Workers / IndexedDB can hold strong references across navigations.
- •WeakRef and WeakMap help when you must keep optional references without preventing GC.
Real-world examples
- •SPAs that navigate without full reloads accumulate listeners + closures over time. Long-running dashboards leak via uncleaned subscriptions. Chat apps leak via event sources that aren't closed on unmount.