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.

  1. After removal, are the nodes collectable?
  2. How much will the JS heap change when the array is finally cleared?
  3. Would performance.memory reveal 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:

InstrumentSees detached DOM?
performance.memory / Runtime.getHeapUsageNo
Heap snapshot (detachedness field)Yes — exact counts
performance.measureUserAgentSpecificMemory()Yes, but needs COOP+COEP
DevTools Memory panel, filter DetachedYes — 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?