Observation Guide

Choosing the instrument first

Most failed leak investigations are instrument-selection failures. Decide what you are looking at before you look.

InstrumentSeesBlind toUse when
performance.memory (Chrome, deprecated)JS heap sizeall DOM, workers, other framesquick triage only; never as evidence
Runtime.getHeapUsage (CDP)JS heap used/totalsamescripted regression checks
Heap snapshotJS heap + native/DOM nodes, detachedness, retainer pathscross-processthe real tool; anything you will act on
performance.measureUserAgentSpecificMemory()whole renderer incl. DOMneeds COOP+COEPproduction RUM, when you can isolate
DevTools Performance → Memory checkboxheap over time, node/listener countsno retainer pathsseeing the shape before you snapshot

Rule: if a claim about a leak rests on performance.memory, it rests on nothing. Measured in this module: 508 KB of detached DOM freed moved the JS heap by 0.10 MB.

The DevTools Memory panel workflow

The workflow that finds leaks, in the order that works:

  1. Establish the repeated action. Open modal → close modal. Navigate → back. It must be idempotent: after one cycle the app should be in the same logical state.
  2. Warm up. Run the action 3–5 times first. Caches, lazy compilation and first-run allocation are not leaks, and skipping this step is how false positives are born.
  3. Snapshot 1. DevTools forces a GC before each snapshot, so you do not need to.
  4. Run the action N times (10–20).
  5. Snapshot 2.
  6. Comparison view, sort by Delta. You are looking for a class whose count grew by exactly N or a multiple of it. That proportionality is the signal — noise does not come in multiples of your loop count.
  7. Select an instance → Retainers panel. This is the finding. Walk the path to a root.
  8. Filter by Detached in the class filter to isolate detached DOM directly.

Reading the columns

  • Shallow size — the object itself. Usually uninteresting.
  • Retained size — what would be freed if it became unreachable. This is the number. A small object with huge retained size is a dominator, and dominators are where fixes go.
  • Distance — hops from a GC root. Very short distances on objects that should be transient are suspicious.
  • Delta (comparison view) — count and size change between snapshots. Sort here first.

Retainer paths worth recognising instantly

window → myModuleCache (Map) → {…} → HTMLDivElement        side-table leak
window → (event listeners) → onResize() → Widget → data    listener leak
window → (timer) → interval callback → context → Widget    timer leak
Window → context → sibling closure → big array             shared-context leak
document → … (nothing)  +  Detached HTMLDivElement         detached DOM

The last one is diagnostic: an element flagged Detached whose retainer path starts at a JS object rather than the document is the definition of the leak.

Healthy vs unhealthy signals

Healthy:

  • Heap returns to roughly its baseline after each cycle, once warmed
  • Detached node count returns to 0 after teardown
  • Listener count stable across mount/unmount cycles
  • Growth slope near zero regardless of R²

Unhealthy, with the diagnosis:

  • Class count growing by exactly N per N cycles → deterministic per-iteration retention
  • Detached nodes accumulating → something holds a reference to removed DOM
  • Listener count rising monotonically → missing teardown, and the target is long-lived
  • Heap sawtooths but the troughs rise → real growth hidden under normal GC activity; look at post-GC minima, not peaks
  • Major GC frequency rising over a session → the fe-01 link: this presents as jank, not as a memory alert

What these measurements do not tell you

  • One browser, one version. Retention behaviour is engine-specific; the closure/context result in particular is a V8 implementation detail, not a language guarantee.
  • Synthetic cycles are not user sessions. Real leaks often need a specific navigation order or an error path that a loop never exercises.
  • Forced GC is not real GC timing. It removes scheduling noise deliberately — which is what makes retention measurable, and what makes these numbers not a model of production pause behaviour.
  • Snapshots perturb what they measure. Taking one forces a GC and pauses the renderer; do not put snapshots inside the loop you are timing.
  • Nothing here covers workers, iframes, or other processes. Each has its own heap, and cross-process retention is invisible to all of the above.