Memory management in JavaScript
JavaScript allocates memory when you create objects and frees it through
garbage collection: an object becomes collectible when no live code can still
reach it. There is no free() — which makes accidental
retention the main failure mode.
Reachability, not reference counting
Modern engines (V8, SpiderMonkey, JavaScriptCore) use tracing collectors: periodically they mark everything reachable from "roots" (globals, current stack, active handles) and sweep the rest. Two objects referencing each other, but unreachable from the roots, are still collected:
function f() {
const a = {}, b = {};
a.other = b; b.other = a; // cycle
return null; // both are collectible
}
Classic leak patterns
- Forgotten timers and listeners: a
setIntervaloraddEventListenerwhose callback closes over big objects keeps them alive forever. - Unbounded caches: a
Mapused as a memo table grows with every distinct key. - Detached DOM nodes: keeping a reference to a removed element in a JS variable prevents its collection.
const cache = new Map();
function memo(key, compute) {
if (!cache.has(key)) cache.set(key, compute(key));
return cache.get(key); // entries live forever
}
WeakMap: keys without retention
A WeakMap holds its keys weakly: an entry disappears
when the key object is otherwise unreachable. Ideal for attaching metadata
without keeping objects alive:
const meta = new WeakMap();
function tag(obj, info) { meta.set(obj, info); }
// when obj is collected elsewhere, its meta entry goes too
WeakSet works the same for membership. Neither is iterable —
by design, since their contents change under the collector.
WeakRef and FinalizationRegistry
WeakRef lets you peek at an object without holding it; use it
for caches where a miss simply recomputes:
let ref = new WeakRef(bigObject);
const maybe = ref.deref(); // object or undefined if collected
FinalizationRegistry runs a callback after collection. Treat
it as a diagnostics tool, not a resource-management mechanism: timing is
unspecified, and the callback itself must not resurrect state.
Quick checklist
- Every
addEventListenerhas a matching removal path. - Timers are cleared in teardown code.
- Unbounded maps are
WeakMaps or have an eviction policy. - Long-lived modules do not close over per-request state.