References and reachability
Garbage collection in JavaScript is reachability-based: if an object can be reached from a GC root, it is considered alive.
GC roots
Section titled “GC roots”Typical roots include:
- The global object (
windowin browsers,globalin 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.
Reference types that confuse developers
Section titled “Reference types that confuse developers”| 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 |
Shallow vs retained size
Section titled “Shallow vs retained size”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.
Broken pattern
Section titled “Broken pattern”const registry = [];
function mountWidget(el) { registry.push(el); document.body.removeChild(el); // DOM gone, but registry keeps it alive}Fixed pattern
Section titled “Fixed pattern”function mountWidget(el, registry) { registry.push(el); return () => { const i = registry.indexOf(el); if (i >= 0) registry.splice(i, 1); };}