Skip to content

WeakMap & Reachability

advanced13 min read

The Cache That Ate Your Memory

You've probably written this exact pattern. It ships in production code every single day:

const cache = new Map();

function getMetadata(element) {
  if (cache.has(element)) return cache.get(element);
  const meta = computeExpensiveMetadata(element);
  cache.set(element, meta);
  return meta;
}

This cache grows forever. Even after DOM elements are removed from the document, the Map holds strong references to them. The elements and their entire subtrees can never be garbage collected. Over hours of user interaction, this silently leaks megabytes.

Replace Map with WeakMap and the leak disappears. Sounds like magic, right? It's not — but the answer requires understanding how V8 decides what's alive and what's garbage.

Reachability: The Only Rule That Matters

Mental Model

Think of memory as a city. The GC roots — global object, call stack, active closures — are city hall. Every object reachable by following references from city hall is a resident and stays alive. Objects with no path from city hall are abandoned buildings. The garbage collector demolishes them. A WeakMap entry is like a sticky note on a building: this building has metadata. But the sticky note alone does not prevent demolition. If no road leads from city hall to the building, it gets demolished — and the sticky note goes with it.

V8's mark-and-sweep algorithm starts from GC roots and walks every reference:

GC Roots (always reachable):
  ├── Global object (window / globalThis)
  ├── Currently executing call stack
  ├── Active closures referenced by the stack
  └── Registered callbacks (setTimeout, event listeners, etc.)

Mark phase: walk all references from roots, mark every object found
Sweep phase: free all unmarked objects

An object is reachable if there is any chain of strong references from a root to it. If not, it's garbage — regardless of how many other dead objects point to it. That's the whole algorithm. Everything else is built on top of this.

Quiz
An object is referenced only by another object that itself has no path from any GC root. Is the first object eligible for garbage collection?

Strong vs Weak References

The distinction is simple but the implications are huge. A strong reference is any normal JavaScript reference — a variable, a property, an array element. Strong references keep objects alive.

A weak reference does not prevent garbage collection. If the only references to an object are weak, the GC can reclaim it. The object just... disappears.

// Strong reference — obj stays alive as long as map exists
const map = new Map();
map.set(key, value); // key is strongly held

// Weak reference — obj can be GC'd even while in the weakmap
const weakmap = new WeakMap();
weakmap.set(key, value); // key is weakly held

WeakMap: Metadata Without Leaks

Now let's get into the specifics. WeakMap has strict constraints, and each one exists for a good reason:

  1. Keys must be objects (or non-registered symbols) — primitives cannot be weakly held because they are not heap-allocated
  2. Keys are held weakly — if nothing else references the key, the entry is removed
  3. Not enumerable — no .keys(), .values(), .entries(), .size. You cannot observe which entries exist without already holding the key
  4. Values are held strongly — but only as long as the key is alive
const metadata = new WeakMap();

(function () {
  const element = document.createElement('div');
  metadata.set(element, { clicks: 0, lastSeen: Date.now() });
  // element is reachable here — entry stays
})();
// element is now unreachable — entry will be removed by GC
Ephemeron Tables in V8

WeakMap is implemented using ephemeron tables — a special GC data structure where entry liveness depends on key liveness. During marking, V8 cannot process WeakMap entries immediately because the key might not be marked yet. Instead, it defers WeakMap processing to a fixpoint loop:

  1. Mark all strongly reachable objects
  2. Scan all WeakMap entries: if a key is marked, mark the value
  3. If any new values were marked in step 2, go back to step 2 (new values might be WeakMap keys)
  4. Repeat until no new marking happens (fixpoint)
  5. Entries with unmarked keys are cleared

This is why WeakMap has slightly more GC overhead than Map — the fixpoint loop adds processing time proportional to WeakMap size.

Real-World Use Cases

Private data per object — the classic pattern before private fields:

const _private = new WeakMap();

class Component {
  constructor() {
    _private.set(this, { renderCount: 0, mounted: false });
  }

  render() {
    const data = _private.get(this);
    data.renderCount++;
  }
}
// When a Component instance is GC'd, its private data is too

DOM element metadata — the cache fix from the introduction:

const elementData = new WeakMap();

function trackElement(el) {
  elementData.set(el, {
    observer: new IntersectionObserver(/* ... */),
    measurements: [],
  });
}
// When el is removed from DOM and all references dropped, data is GC'd
Quiz
Why can't a WeakMap use primitive keys like strings or numbers?

WeakSet: Membership Without Retention

WeakSet is simpler — it tracks whether an object is in the set, without holding it alive:

const processed = new WeakSet();

function processOnce(obj) {
  if (processed.has(obj)) return;
  processed.add(obj);
  // ... do work
}
// When obj is GC'd, it is automatically removed from processed

Use cases: tracking visited nodes in graph traversal, preventing double-processing, branding objects.

WeakRef: Explicit Weak References

WeakRef is the most direct tool in this family. It gives you a weak reference to a single object. Call .deref() to get the object — or undefined if it has been collected:

let ref = new WeakRef(expensiveObject);

// Later...
const obj = ref.deref();
if (obj) {
  // Still alive — use it
  obj.doSomething();
} else {
  // Collected — recreate or handle absence
}
Common Trap

Do not use WeakRef for caching. The GC is non-deterministic — your cache might be cleared at any time, even mid-function. WeakRef is for cases where you genuinely do not care when the object disappears, like UI observation patterns. For caching, use an LRU cache with explicit size limits.

FinalizationRegistry: Cleanup on Collection

Last one, and this one comes with a big caveat. FinalizationRegistry lets you register a callback that runs after an object is garbage collected:

const registry = new FinalizationRegistry((heldValue) => {
  console.log(`Object for ${heldValue} was collected`);
  // Clean up external resources
});

function createResource(id) {
  const resource = acquireExternalResource(id);
  registry.register(resource, id); // id is the "held value"
  return resource;
}
Info

FinalizationRegistry callbacks are not guaranteed to run. They are best-effort. The browser may shut down, the page may navigate away, or the GC might not run. Never rely on finalization for correctness — only for opportunistic cleanup of external resources.

Quiz
You register an object with a FinalizationRegistry. When does the callback fire?

Putting It All Together

Key Rules
  1. 1Map/Set — strong references. Keys and values stay alive as long as the collection exists
  2. 2WeakMap/WeakSet — weak keys. Entries are removed when the key is garbage collected. Use for associating metadata with objects without preventing their collection
  3. 3WeakRef — weak reference to a single object. .deref() returns the object or undefined. Use sparingly — not for caching
  4. 4FinalizationRegistry — best-effort cleanup callback after collection. Never rely on it for correctness
  5. 5Reachability — the only thing that matters. If no chain of strong references connects an object to a GC root, it is garbage
Quiz
You have a Map storing DOM elements as keys with associated data as values. Elements are dynamically added and removed from the page. What happens over time?
Interview Question

Q: When would you use WeakMap over Map?

When you need to associate data with objects whose lifetime you do not control. The canonical examples: DOM element metadata, caching computed values for objects that might be removed, and private data associated with class instances. The key insight is that a regular Map creates an ownership relationship — it keeps objects alive. A WeakMap creates an observation relationship — it watches objects without affecting their lifetime.