Step 2 — Detached DOM, and Why Your Instrument Lied
Goal
Find a detached-DOM leak, and learn that the tool most teams reach for cannot see it.
Prerequisites
- Step 1 complete
Predict first
5,000 <div> elements are created, appended to the document, then removed with
host.replaceChildren() — while an array holds a JS reference to every one.
- After removal, are the nodes collectable?
- How much will the JS heap change when the array is finally cleared?
- Would
performance.memoryreveal this leak?
Run
npm run detached
Expected output:
stage | detached nodes | detached bytes | JS heap
---------------------------------|----------------|----------------|--------
built + attached | 0 | 0.0KB | 0.84MB
removed from DOM, JS refs held | 5000 | 507.8KB | 0.84MB
JS refs released | 0 | 0.0KB | 0.74MB
What just happened
Removing a node from the document does not free it. It only removes one reference path — the
one from document. The array still reaches every node, so every node is retained, along with its
attributes, text nodes, and any listeners attached to it.
And the JS heap barely moved. Freeing 507.8 KB of DOM changed the JS heap by 0.10 MB, because
DOM nodes live in Blink's C++ heap (Oilpan), not V8's. Runtime.getHeapUsage,
performance.memory, and every "check the heap size" reflex report the JS heap only.
This is the module's central methodological point:
A team checks
performance.memory, sees a flat line, concludes "no leak", and stops looking. The instrument selected the conclusion.
What actually sees it:
| Instrument | Sees detached DOM? |
|---|---|
performance.memory / Runtime.getHeapUsage | No |
Heap snapshot (detachedness field) | Yes — exact counts |
performance.measureUserAgentSpecificMemory() | Yes, but needs COOP+COEP |
DevTools Memory panel, filter Detached | Yes — with retainer paths |
Detached subtrees retain their whole subtree. One held reference to a leaf can retain thousands
of ancestors. This is why virtualized lists (fe-30) turn a small mistake into an incident.
Checkpoint
docs/verification.md Checkpoint 2.
Going deeper
ROWS=50000 npm run detached
Then ask: how large would this leak need to be before performance.memory moved enough to notice —
and would the page still be usable at that point?