DNA⚡ JavaScriptMemory Management & Garbage Collection
ðŸĶ–DinosaurJavaScriptPerformanceInternals

Memory Management & Garbage Collection

Memory leaks in JavaScript are silent killers. Senior engineers understand the GC algorithm, common leak patterns, and the tools to detect them before users notice.

Memory Management & Garbage Collection

JavaScript developers rarely think about memory — until their SPA has been open for 4 hours and the tab consumes 2GB. Understanding how memory works, how garbage collection operates, and where leaks hide is senior-level knowledge.

Memory Lifecycle

Allocate → Use → Release (GC)
 
1. Allocation: Creating variables, objects, functions, closures
2. Use: Reading and writing to allocated memory
3. Release: Garbage collector reclaims unreachable memory

What Gets Allocated Where

TypeStorageNotes
Primitives (number, string, boolean)Stack (usually)Small, fixed size, copied by value
Objects, arrays, functionsHeapDynamic size, passed by reference
ClosuresHeapRetain their lexical environment

Garbage Collection: Mark-and-Sweep

Modern engines (V8, SpiderMonkey) use a mark-and-sweep algorithm:

1. Start from "roots" (global object, call stack, active closures)
2. Mark all objects reachable from roots
3. Sweep (free) all unmarked objects
 
Roots ──→ Object A ──→ Object C
     ──→ Object B
 
Object D (unreachable) → Collected
Object E → Object D    → Both collected

V8's Generational GC

V8 divides the heap into two generations:

┌─────────────────────────┐  ┌─────────────────────────────────┐
│      Young Generation    │  │       Old Generation             │
│  (Scavenger - fast, frequent) │  │  (Mark-Sweep-Compact - slow, rare) │
│                          │  │                                  │
│  New objects created here │  │  Objects that survived 2+        │
│  Small space (~1-8MB)    │  │  scavenger cycles               │
│  Collected every few ms  │  │  Large space (100s MB)           │
└─────────────────────────┘  └─────────────────────────────────┘

Most objects die young (temporary variables, short-lived callbacks). The scavenger collects young generation frequently and cheaply. Objects that survive multiple scavenger cycles get promoted to old generation.

The Five Common Memory Leak Patterns

1. Forgotten Event Listeners

// ❌ Listener survives component — references keep growing
function setupTracker() {
  const data = new Array(10000).fill('tracking');
 
  window.addEventListener('scroll', () => {
    process(data); // `data` is retained as long as listener exists
  });
}
setupTracker(); // Called on every route change? Listeners accumulate
 
// ✅ Clean up
function setupTracker() {
  const data = new Array(10000).fill('tracking');
  const handler = () => process(data);
  window.addEventListener('scroll', handler);
  return () => window.removeEventListener('scroll', handler);
}
const cleanup = setupTracker();
cleanup(); // Remove listener and allow data to be GC'd

2. Orphaned Closures

// ❌ Closure retains large data indefinitely
function createProcessor() {
  const cache = new Map();
  const hugeBuffer = new ArrayBuffer(50_000_000); // 50MB
 
  return {
    process(key) {
      if (cache.has(key)) return cache.get(key);
      const result = heavyComputation(key);
      cache.set(key, result);
      return result;
    },
    // hugeBuffer is retained because it's in the same closure scope
    // even though process() never uses it!
  };
}
 
// ✅ Separate concerns — don't capture unnecessary data
function createProcessor() {
  const cache = new Map();
  return {
    process(key) {
      if (cache.has(key)) return cache.get(key);
      const result = heavyComputation(key);
      cache.set(key, result);
      return result;
    },
  };
}

3. Growing Collections (Unbounded Caches)

// ❌ Map grows forever
const cache = new Map();
function getData(key) {
  if (!cache.has(key)) {
    cache.set(key, fetchData(key));
  }
  return cache.get(key);
}
 
// ✅ LRU cache with size limit
function createLRUCache(maxSize = 100) {
  const cache = new Map();
 
  return {
    get(key) {
      if (!cache.has(key)) return undefined;
      const value = cache.get(key);
      cache.delete(key);
      cache.set(key, value);
      return value;
    },
    set(key, value) {
      cache.delete(key);
      cache.set(key, value);
      if (cache.size > maxSize) {
        const oldest = cache.keys().next().value;
        cache.delete(oldest);
      }
    },
  };
}

4. Detached DOM Nodes

// ❌ DOM node removed but still referenced
let detachedNode;
function replaceContent() {
  detachedNode = document.getElementById('old-content');
  document.body.removeChild(detachedNode);
  // detachedNode still holds the entire subtree in memory
}
 
// ✅ Don't hold references to removed DOM nodes
function replaceContent() {
  const oldContent = document.getElementById('old-content');
  document.body.removeChild(oldContent);
  // oldContent goes out of scope → eligible for GC
}

5. Forgotten Timers

// ❌ Interval runs forever — retains callback and its closure
const data = fetchHugeDataset();
setInterval(() => {
  updateUI(data); // data retained as long as interval runs
}, 1000);
 
// ✅ Clear when no longer needed
const data = fetchHugeDataset();
const id = setInterval(() => updateUI(data), 1000);
// Later:
clearInterval(id);

WeakMap & WeakSet — GC-Friendly Collections

WeakMap and WeakSet hold weak references — they don't prevent garbage collection of their keys:

const metadata = new WeakMap();
 
function processElement(element) {
  metadata.set(element, {
    processedAt: Date.now(),
    interactions: 0,
  });
}
 
// When `element` is removed from the DOM and no other reference exists,
// both the element AND the metadata are garbage collected

Use Cases

// Cache that doesn't leak — entries disappear when keys are GC'd
const computationCache = new WeakMap();
 
function expensiveCompute(obj) {
  if (computationCache.has(obj)) return computationCache.get(obj);
  const result = /* heavy work */ obj.data.reduce((a, b) => a + b);
  computationCache.set(obj, result);
  return result;
}
 
// Private data per instance (classic pattern before #private fields)
const privates = new WeakMap();
 
class User {
  constructor(name, ssn) {
    this.name = name;
    privates.set(this, { ssn });
  }
 
  getSSN() {
    return privates.get(this).ssn;
  }
}

WeakRef & FinalizationRegistry

For advanced scenarios where you need a reference that doesn't prevent GC:

const cache = new Map();
 
function getCached(key, compute) {
  const ref = cache.get(key);
  if (ref) {
    const value = ref.deref();
    if (value !== undefined) return value;
  }
 
  const value = compute(key);
  cache.set(key, new WeakRef(value));
  return value;
}
 
// FinalizationRegistry — run cleanup when objects are GC'd
const registry = new FinalizationRegistry((heldValue) => {
  cache.delete(heldValue);
  console.log(`Cleaned up cache entry: ${heldValue}`);
});
 
function cacheObject(key, obj) {
  cache.set(key, new WeakRef(obj));
  registry.register(obj, key);
}

Detecting Memory Leaks

Chrome DevTools Memory Tab

  1. Heap Snapshot — Take snapshots before and after an action, compare to find growing objects
  2. Allocation Timeline — Shows when memory is allocated; spikes without drops = leaks
  3. Allocation Sampling — Low-overhead profiling for production

The Three-Snapshot Technique

1. Take Heap Snapshot (baseline)
2. Perform the suspected leaky action
3. Take Heap Snapshot
4. Undo the action (navigate away, close modal)
5. Take Heap Snapshot
6. Compare Snapshot 1 and 3 — objects in 3 but not 1 are leaks

Performance.memory API (Chrome)

if (performance.memory) {
  console.log({
    usedJSHeapSize: performance.memory.usedJSHeapSize,
    totalJSHeapSize: performance.memory.totalJSHeapSize,
    jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
  });
}

Interview Signal

Senior candidates demonstrate:

  1. GC algorithm knowledge — Mark-and-sweep, generational GC, young vs old generation
  2. Leak pattern recognition — The five common patterns and how to prevent each
  3. WeakMap/WeakRef usage — When and why to use weak references
  4. Detection skills — Heap snapshots, three-snapshot technique, allocation timeline
  5. Practical awareness — SPAs accumulate state; navigation should release memory