Memory Leaks & V8 GC
Key Learning Objectives
Identify the five primary causes of JavaScript memory leaks in SPAs: detached DOM nodes, forgotten timers, closures, accidental globals, and unremoved listeners.
Understand the V8 Garbage Collection architecture: generational hypothesis, young generation Scavenge, and old generation Mark-Sweep-Compact.
Master non-deterministic GC behavior: recognize why GC runs based on runtime memory pressure and allocation thresholds rather than fixed schedules.
Analyze memory retaining paths using Chrome DevTools Heap Snapshots: Shallow Size, Retained Size, and Distance from GC Roots.
Implement leak mitigation techniques including WeakMap, explicit listener teardown, and AbortSignal event listeners.
The Interview Problem
What is logged to the console when an object is tracked in a root collection, its local variable binding is reassigned to null, and it is subsequently detached from the collection root?
1class MemoryTracker {2 constructor() {3 this.retainedRoots = new Set();4 }5 attach(obj) {6 this.retainedRoots.add(obj);7 }8 detach(obj) {9 this.retainedRoots.delete(obj);10 }11 isReachable(obj) {12 return this.retainedRoots.has(obj);13 }14}1516const tracker = new MemoryTracker();1718let detachedDiv = { tag: 'div', id: 'dialog' };19tracker.attach(detachedDiv);2021const ref1 = tracker.isReachable(detachedDiv);2223const originalRef = detachedDiv;24detachedDiv = null;25const ref2 = tracker.isReachable(originalRef);2627tracker.detach(originalRef);28const ref3 = tracker.isReachable(originalRef);2930console.log(ref1, ref2, ref3, detachedDiv === null);
Predict Console Output
Select the option that matches what standard ECMAScript prints to the console:
true true false true
true false false true
true true true false
false false false true
V8 Engine Execution Trace
Step 1 of 6 (Line 16)Instantiates MemoryTracker with a retainedRoots Set simulating a root retention structure.
Deep Technical Breakdown
Anatomy of JavaScript Memory Leaks & V8 Garbage Collection
A memory leak in JavaScript is not allocated memory that cannot be freed (as in C/C++); it is unwanted memory retained by active references connected to GC Roots:
1. What are GC Roots?
The V8 Garbage Collector starts from a set of known roots:
- Global variables (attached to
windoworglobalThis). - Current Call Stack (local variables and parameters of executing functions).
- Built-in runtime objects and active DOM trees.
An object is reachable (and thus protected from GC) if a chain of property or closure references leads back to any GC Root.
2. The Five Classic Frontend Memory Leaks
- Detached DOM Trees: An element is removed from the DOM with
node.remove(), but a JavaScript variable, array, or event callback still references it. The entire subtree remains in memory. - Forgotten Timers & Intervals:
setInterval(() => { ... }, 1000)retains its closure scope forever untilclearInterval()is explicitly invoked. - Closures Sharing Lexical Environments: If an outer function creates multiple closures, some engines share the lexical context. An unused closure retaining a large array or buffer can keep that memory alive if another small closure survives.
- Unremoved Event Listeners: In SPAs, subscribing to
window.addEventListener('resize', handler)without callingremoveEventListenerwhen the component unmounts leaks the component and its scope. - Accidental Globals: Declaring variables without
let/const(foo = 'bar') or assigning tothisinside an unbound function in non-strict mode.
3. How V8 Garbage Collection Works (Non-Deterministic Guarantee)
- Generational Hypothesis: Most objects die young. V8 divides the heap into:
- Young Generation (Nursery & Intermediate): Small (1-64MB). Collected frequently and rapidly using Scavenge (Cheney's copying algorithm).
- Old Generation: Objects surviving two Scavenges are promoted. Collected using Mark-Sweep-Compact (major GC).
- NON-DETERMINISTIC BEHAVIOR: Garbage collection does NOT run immediately when a variable is dereferenced. It runs non-deterministically based on heap allocation pressure, idle task scheduling, and memory limits.
4. Chrome DevTools Heap Profiler Metrics
- Shallow Size: Memory directly held by the object itself (byte array headers, primitive slots).
- Retained Size: Memory that would be freed if this object and its dependent reference tree were deleted.
- Distance: The shortest path of hops from a GC Root to this object. Higher distance often indicates deeply nested data.
Common Traps & Mistakes
Believing that garbage collection is deterministic or occurs at predictable timestamps. V8 GC is non-deterministic and triggered by memory pressure and allocation thresholds.
Assuming setting `obj = null` immediately frees heap memory. If any other variable, listener, or collection holds a reference, memory is retained.
Removing an element with `element.remove()` while forgetting that a detached DOM reference is still stored in an array or event handler.
Failing to clean up `setInterval` or `addEventListener` bindings when frontend views unmount.
FAANG Follow-Up Probes
Probe #1
How does the Chrome DevTools 'Allocation instrumentation on timeline' differ from taking two comparative Heap Snapshots?
Probe #2
Why are WeakMap and WeakSet preferred for associating metadata with DOM elements or objects without preventing garbage collection?
Probe #3
How does the AbortSignal pattern simplify event listener cleanup: element.addEventListener('click', fn, { signal: controller.signal })?
