Memory and garbage collection
Reachability, the three classic leaks, and matching teardowns.
JavaScript frees what you can't reach and keeps everything else — so memory management is reference management. Interviewers ask this because the three classic leaks (timers, detached DOM, growing caches) all reduce to one forgotten reference.
Real-life situation
A dashboard caches every view it opens "for speed" and never prunes. After an hour the tab crawls: each cached view is still reachable, so the collector can't free a byte. The fix is a matching teardown — cap the cache, clear it on logout — the same discipline as clearing timers and removing listeners.
Visual
Reachable survivesbrowser
Roots
Heap objects
Marked (alive)
empty
Collected
—
Everything is reachable. cart points at the cart object; the subscribed listener points at the handler. Roots are variables the program can still touch.
let cart = { items: ["book"] }; // reachable: cart points at it
let oldListener = () => save(cart); // reachable while subscribed
unsubscribe(oldListener); // now nothing points at it
cart = null; // now nothing points at the cart
// garbage collector runs…Try it
const cache = [];
function remember(view) {
cache.push(view);
}
function openView(name) {
const view = { name: name, data: [1, 2, 3] };
remember(view);
return view.name;
}
console.log(openView("home"));
console.log(openView("search"));
console.log(cache.length);
cache.length = 0;
console.log(cache.length);
const listeners = [];
function onEvent(handler) {
listeners.push(handler);
}
onEvent(function () {
return "click";
});
onEvent(function () {
return "hover";
});
console.log(listeners.length);
listeners.length = 0;
console.log(listeners.length);Expected output
home search 2 0 2 0
Try commenting out cache.length = 0 and watch the count stick. Then give
remember a size cap that drops the oldest view past five entries.
When it applies
Any long-lived page: dashboards, editors, chats. Short-lived scripts rarely leak enough to matter — but the teardown habit (every setup gets an undo) pays off in components, where mount/unmount repeats the cycle constantly.
Held by a closure — guess first, then reveal.
function subscribe() {
const big = [1, 2, 3];
return function () {
return big.length;
};
}
const read = subscribe();
console.log(read());Interview questions
How does JavaScript free memory?basic
Automatically, by reachability: the garbage collector periodically traces from roots (globals, active stack frames) and frees whatever it cannot reach. You never free() anything — you make things UNREACHABLE by dropping references (clear timers, remove listeners, null caches). Memory management in JS is reference management; leaks are just references you forgot.
Interviewers also ask
- What counts as a GC root?
What are the three most common JavaScript memory leaks?intermediate
Forgotten timers (setInterval outliving its feature), detached DOM kept alive by listeners or caches (removed element, still referenced), and ever-growing global collections (caches and arrays nobody prunes). Each is a reference that outlives its purpose. The fix pattern is uniform: every setup (timer, listener, subscription) needs a matching teardown on the same lifecycle.
Interviewers also ask
- How do you find a leak in DevTools?
What is the difference between stack and heap memory?basic
The stack holds fixed-size execution data — call frames, primitives, and references — created and destroyed automatically as functions run. The heap holds the objects those references point at, living as long as anything reaches them. Stack overflow comes from too many calls; heap growth comes from too many reachable objects. Interviews pair them to check you know WHERE values live, not just what they are.
Interviewers also ask
- What lives on the stack versus the heap?
Why do removed DOM elements sometimes stay in memory?intermediate
Removing an element from the document only detaches it; any live reference — an event listener, a variable, a cache entry — keeps the whole subtree reachable and therefore uncollectable, listeners and all. Fix: remove listeners (or use { once: true }/delegation so there is nothing to remove), and drop cache entries when views unmount. Detached-but-referenced is the classic DevTools heap-snapshot find.
Interviewers also ask
- How does event delegation help here?
How does mark-and-sweep collection work, briefly?intermediate
In three beats: MARK everything reachable by tracing from the roots; SWEEP (free) everything unmarked; optionally COMPACT survivors together to fight fragmentation. Modern engines run it incrementally and in parallel so pauses stay short. You do not need engine internals for interviews — "reachable survives, unreachable is freed" plus the three-beat name is the complete expected answer.
Interviewers also ask
- Why do GC pauses happen at all?
Do you need to null out variables to help the GC?basic
Rarely for locals — when a function returns, its frame (and its references) vanish automatically. Nulling matters only for LONG-lived holders: globals, module-level caches, and closures that outlive their usefulness. The rule: clean up at teardown (clear timers, unsubscribe, empty the cache slot), and let scope exit handle the rest.
Interviewers also ask
- When is setting null actually the fix?
Key takeaways
- Reachable survives; unreachable is freed — leaks are forgotten references.
- The big three: stray timers, detached DOM, unbounded caches.
- Every setup needs a teardown on the same lifecycle.
References
Last verified: 2026-10-12.
Runner helpers
sleep(ms)waits;fakeFetch(url)returns deterministic sample data. Console code runs in a worker with a 5-second limit.