Skip to content

References and reachability

Garbage collection in JavaScript is reachability-based: if an object can be reached from a GC root, it is considered alive.

Typical roots include:

  • The global object (window in browsers, global in Node)
  • Current call stack local variables
  • Closures capturing outer variables
  • Registered handles — timers, event listeners, open sockets (Node)
flowchart TD
  root[GC Root: global]
  arr[leakedArray]
  obj1[Object A]
  obj2[Object B]
  root --> arr
  arr --> obj1
  arr --> obj2
  orphan[Unreachable object]
  style orphan stroke-dasharray: 5 5

orphan has no path from any root → eligible for collection.

Pattern Why it retains
const cache = new Map() at module scope Module scope ≈ lives forever
Closure in setInterval Interval handle is a root → closure → captured vars
DOM ref in array after removeChild JS reference keeps subtree alive (“detached DOM”)
eventBus.on('x', handler) without .off Emitter holds handler → closure scope

When debugging heap snapshots you’ll see:

  • Shallow size — memory held by the object itself
  • Retained size — memory freed if this object were removed (includes objects only reachable through it)

A small array of 10,000 DOM nodes has tiny shallow size but huge retained size.

const registry = [];
function mountWidget(el) {
registry.push(el);
document.body.removeChild(el); // DOM gone, but registry keeps it alive
}
function mountWidget(el, registry) {
registry.push(el);
return () => {
const i = registry.indexOf(el);
if (i >= 0) registry.splice(i, 1);
};
}