Memory Leaks & V8 GC

Hard100% Free~30 mins#memory-leaks#garbage-collection#v8#heap-snapshot#retained-size#detached-dom#performance
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}
15
16const tracker = new MemoryTracker();
17
18let detachedDiv = { tag: 'div', id: 'dialog' };
19tracker.attach(detachedDiv);
20
21const ref1 = tracker.isReachable(detachedDiv);
22
23const originalRef = detachedDiv;
24detachedDiv = null;
25const ref2 = tracker.isReachable(originalRef);
26
27tracker.detach(originalRef);
28const ref3 = tracker.isReachable(originalRef);
29
30console.log(ref1, ref2, ref3, detachedDiv === null);
Predict Console Output
Interactive Challenge

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.

Call Stack (Top = Active)
Global Execution Context
Lexical Scope / Bindings
tracker:MemoryTracker { retainedRoots: Set(0) }
Console Stream
> [empty]

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 window or globalThis).
  • 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

  1. 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.
  2. Forgotten Timers & Intervals: setInterval(() => { ... }, 1000) retains its closure scope forever until clearInterval() is explicitly invoked.
  3. 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.
  4. Unremoved Event Listeners: In SPAs, subscribing to window.addEventListener('resize', handler) without calling removeEventListener when the component unmounts leaks the component and its scope.
  5. Accidental Globals: Declaring variables without let/const (foo = 'bar') or assigning to this inside 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 })?