Step 4 — Side Tables, Caches, and the Limits of WeakMap
Goal
Measure the most common accidental cache leak, and establish when WeakMap is not the answer.
Prerequisites
- Steps 1–3 complete
Predict first
2,000 DOM nodes are created, used as keys in a module-level side table with per-node metadata, then detached and dereferenced. Two variants, identical except:
const store = new Map(); // vs
const store = new WeakMap();
Predict retained heap and surviving detached-node count for each.
Run
npm run weakmap
Expected output:
side table | retained JS heap | detached nodes still alive
-----------|------------------|---------------------------
Map | 38.34MB | 2000
WeakMap | 0.09MB | 0
What just happened
A Map holds its keys strongly. Every node the map has ever seen is immortal, along with the
metadata hanging off it. The nodes are unreachable from the document and from application code,
and still not collectable — the map is the only thing keeping 38 MB alive.
This is the shape of most accidental caches: a module-level Map, Set, registry, or
id → object index that nothing ever deletes from.
A cache without an eviction policy is a leak with better branding.
But WeakMap is not a default. Five limits worth knowing before reaching for it:
- It weakens the key, not the value.
wm.set(node, { node })retains everything, because the value's reference to the key is strong. Easy to write, hard to see. - Not enumerable. No
.size, no iteration, no dump during an incident. You have traded a visible leak for an invisible cache. - Object keys only. Anything keyed by an id string cannot use it.
- Non-deterministic collection. You cannot write a test asserting an entry was freed — only that growth is bounded, which is a weaker claim.
- It implements one policy: "when the key dies". If entries should expire on logout, on
navigation, or after 30 seconds,
WeakMapimplements none of that.
The decision rule:
| The entry should die when… | Use |
|---|---|
| its key object dies | WeakMap |
| a fixed time passes | TTL cache |
| memory is the binding constraint | LRU |
| a specific event occurs (logout, tenant switch) | explicit clear-on-event |
Point 5 crosses into security: cached user data surviving a logout is a privacy finding, not a
memory one, and WeakMap would not have fixed it. Picked up in fe-25.
Checkpoint
docs/verification.md Checkpoint 4.