What they're testing
Whether you have a diagnostic procedure and can use heap snapshots. It's very hard to fake if you haven't done it.
The short answer~30 seconds
I measure first: take a heap snapshot, repeat one user flow a few dozen times, force GC, take a second snapshot and diff on "objects allocated between snapshots". Whatever survived that shouldn't have is the lead, and the retainer tree names who's holding it. The four usual culprits: listeners never removed, timers never cleared, references to detached DOM nodes, and a hand-rolled cache with no bound.
The long answer
What makes this measurement trustworthy is three snapshots rather than two. Snapshot, run the flow, snapshot, run it again, snapshot — then diff the third against the second. That drops one-time initialisation that pollutes a two-snapshot diff. If the memory line climbs in steps and never returns, that's a leak; if it saws around a level, that's ordinary GC behaviour.
Detached DOM is the hardest kind to see. You remove a node from the tree, but an array inside a closure still references it, so the entire subtree — possibly thousands of nodes — stays in the heap while appearing nowhere. DevTools has a "Detached" filter in heap snapshots for precisely this. In React the cause is usually a ref stored in global state, or a window listener whose useEffect returns no cleanup.
On hand-rolled caches: a Map used as a memo store with neither a size cap nor a TTL isn't a cache, it's a deliberate leak. A WeakMap helps when the key is an object and you want the entry to vanish with it, but WeakMap doesn't help for string-keyed caches. Those need a real bounded LRU.
A senior-level caveat: rising memory isn't automatically a leak. V8's GC collects when it decides to, so an upward line over a few minutes is normal. You should only call it a leak when, after forcing GC, the baseline is still higher than before — and saying so shows you can tell a symptom from a cause.
Four leak sources, and their fixes
useEffect(() => {
const onResize = () => layout(rows);
window.addEventListener('resize', onResize);
const id = setInterval(poll, 5000);
const ac = new AbortController();
fetch(url, { signal: ac.signal });
return () => { // <- thiếu dòng nào cũng là rò rỉ
window.removeEventListener('resize', onResize);
clearInterval(id);
ac.abort();
};
}, [rows]);
// Cache có giới hạn, không phải Map vô hạn
const cache = new Map<string, Result>();
if (cache.size > 500) cache.delete(cache.keys().next().value);What they'll ask next
?What does a WeakMap buy you?
Keys are held weakly: once nothing else references the key object, the entry disappears without you deleting it. Ideal for attaching side data to objects you don't own. It isn't enumerable and has no size — that's the price.
?How do you measure this in production?
performance.memory is unreliable and clamped; the better API is measureUserAgentSpecificMemory(), which needs cross-origin isolation. In practice many teams measure indirectly: the rate of browser-killed tabs, and session length before performance degrades.
These lose points
- Proposing "set it to
nullto free memory" as the main fix. GC works on reachability; nulling helps only when it severs the last reference. - Guessing the cause without taking a snapshot. Leaks are a measurement problem, not a reasoning problem.