50. Garbage collection and memory leaks in real JS apps
V8 uses a generational garbage collector, based on the empirical observation that most objects die young: two heap generations, collected with different strategies.
Young generation (small, "Scavenger" GC — fast, frequent)
- new objects allocated here first
- most die almost immediately (a temporary object in a function call) ->
cheap to collect because most of the generation IS garbage
- objects that SURVIVE a couple of scavenges get PROMOTED to:
Old generation (large, "Mark-Sweep-Compact" GC — slower, less frequent)
- long-lived objects: app state, caches, closures held for a page's lifetime
- collected less often since triggering a full mark-sweep is expensive
Memory leaks in JS aren't "forgotten free()" bugs (there's no manual
free) — they're references you didn't mean to keep, which prevent the
GC from ever considering an object garbage. The recurring real-world
patterns:
// 1. Detached DOM nodes: removing an element from the document but keeping
// a JS reference to it anywhere (a cache, an event-handler closure) keeps
// its ENTIRE subtree alive in memory even though it's invisible.
const detachedCache = [];
function removeAndLeak(el) {
el.remove(); // gone from the visible DOM...
detachedCache.push(el); // ...but still alive in memory via this array
}
// 2. Forgotten event listeners / timers holding closures over large state
function setupLeak(largeData) {
window.addEventListener("resize", () => console.log(largeData.length));
// if this component is ever "destroyed" without removeEventListener,
// `largeData` is held alive forever by the still-registered listener
}
// 3. Growing caches/maps with no eviction — a Map that only ever grows
// is a slow, "successful" memory leak by design, just not called one
const cache = new Map();
function memoize(key, computeFn) {
if (!cache.has(key)) cache.set(key, computeFn()); // never evicted -> unbounded growth
return cache.get(key);
}
Diagnosing leaks with DevTools: Memory panel → take a heap snapshot →
perform the suspected leaking action several times → take another snapshot
→ use "Comparison" view to see which object types grew between snapshots
that shouldn't have — a detached HTMLDivElement count climbing steadily is
the single most common finding in real leak hunts.