Verification Checkpoints
Reference environment: Chrome for Testing 149.0.7827.55, arm64 macOS. Ranges are the pass condition; exact bytes are machine-specific.
Checkpoint 1 — Closures retain contexts, not variables
npm run closures
variant | retained heap
A no sibling references it | 0.01MB
B unused sibling references it | 38.17MB
C sibling + explicit big = null | 0.01MB
Pass: B is at least 100× A. A and C are within noise of each other (< 0.5 MB).
Fail — all three near zero: SIZE too small, or the engine optimised the arrays away. Raise
SIZE/N.
Fail — A is also large: you are retaining something else; check that __held is reset between
variants.
Checkpoint 2 — The JS heap does not see DOM
npm run detached
removed from DOM, JS refs held | 5000 | 507.8KB | 0.84MB
JS refs released | 0 | 0.0KB | 0.74MB
Pass: detached count goes 0 → ROWS → 0, while the JS-heap column changes by a much smaller
amount than the detached-bytes column.
Fail — detached always 0: the snapshot has no detachedness field (older V8). lib/heap.mjs
reports hasDetachednessField; check it. Fall back to name-prefix matching only if false.
This checkpoint is the one to internalise: the instrument selects the conclusion.
Checkpoint 3 — Listener lifecycle, three variants
npm run listeners
anonymous listener, never removed | 23.00MB | 300
removeEventListener in destroy() | 0.08MB | 0
AbortController signal + abort() | 0.09MB | 0
Pass: the anonymous variant retains ≥ 20× the other two and shows CYCLES detached nodes
while the other two show 0.
Fail — all three show the same detached count: page isolation is broken; each variant must run
on a fresh page. This exact bug occurred while building the module.
Checkpoint 4 — WeakMap weakens keys
npm run weakmap
Map | 38.34MB | 2000
WeakMap | 0.09MB | 0
Pass: Map retains ≥ 100× the WeakMap variant, and detached counts differ by ~N.
Checkpoint 5 — Slope and linearity together
npm run growth
clean ▁▁▁▁▁▁▁█████ slope 0.001 MB/cycle R² 0.742
leaky ▁▁▂▃▃▄▅▆▆▇██ slope 1.836 MB/cycle R² 1.000
Pass: leaky slope ≥ 0.5 MB/cycle with R² ≥ 0.95; clean slope < 0.05 MB/cycle. Note the clean R² is high — around 0.74 here — and that is the point. If your clean R² comes out low, run it again; over a short series it will sometimes be high. The checkpoint is that you can state why a high R² on the clean run is not alarming (the slope is at zero magnitude).
Checkpoint 6 — You can name the retainer path
Not a script. Open web/leaky-app.html, take two snapshots around a repeated action, use the
Comparison view, and for each of the four seeded leaks write down:
- the object class that grew,
- the retainer path from a GC root,
- the single edge you would break to fix it,
- whether breaking that edge is a local fix or an architectural one.
Pass: four retainer paths written down, each naming a specific root. "It's a closure" is not a
retainer path. window → appState.cache → Map → {…} → HTMLDivElement is.
Module completion
All six checkpoints, and:
-
steps/06-principal-review.mdanswered in writing - One leak found in a real codebase you work on, or a documented statement of why none was found and what you checked — the method transfers or it was not learned
-
../fe-00-roadmap/docs/learning-log.mdupdated with any prediction you got wrong -
A soak-test sketch: what you would assert, at what cadence, in CI (
fe-33picks this up)