learn
JavaScript

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

oldListener
cart

Heap objects

{ items: ["book"] } ← cart
handler ← oldListener

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.

Step 1 of 5
scenario.js
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

Cache that never forgets
javascriptTab moves to the next control
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);
Press Run to see the output.
Expected output
home
search
2
0
2
0
Runner helpers

sleep(ms) waits; fakeFetch(url) returns deterministic sample data. Console code runs in a worker with a 5-second limit.

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.

predict.js
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.

On this page