Part 6 · 2 chapters · ~25 min
Memory
Three heap snapshots and the comparison view that separates a leak from a cache, retainers that name the holder, the Summary columns that matter, allocation timelines and sampling, the Performance monitor as the early-warning instrument, the sawtooth, and the leak shapes mapped to the tool that finds each.
18
Heap snapshots: three, not one
the method
- Baseline: let the page settle, force GC (the bin icon), take snapshot 1.
- Round one: perform the suspect action and its undo ten times (open/close, mount/unmount, navigate/back). Snapshot 2. Growth here proves nothing: caches, lazy modules and memo tables fill once.
- Round two: the same ten cycles. Snapshot 3. Growth that repeats is a leak; a cache would show ~0 the second time. The delta per round is the leak's rate.
- Compare: select snapshot 3, switch Summary to "Objects allocated between Snapshot 2 and Snapshot 3": only objects born in round two and still alive. Sort by Retained Size, not Shallow: the small object holding a big subtree is the one to find.
- Retainers: click an object; the pane shows the chain back to a GC root. Walk it to the first frame that is your code: a module-level Map, a listener on window, a closure in a long-lived store, a global array. That is the leak.
reading the Summary view
- Constructor (class or shape;
(string),(array),(compiled code),systemare engine internals and usually not yours), Distance (hops from a root), Shallow Size (the object), Retained Size (the object plus everything only it keeps alive). - The class filter "Detached" lists DOM nodes no longer in the document but still referenced: the most common frontend leak. Start there.
- A snapshot pauses the page for seconds on a large heap; that is expected. Snapshots of a production tab with sensitive data contain that data: handle the files accordingly.
go to the lab
/js/detached-list: baseline snapshot; cycle ×10; snapshot; cycle ×10; snapshot. Compare 3 to 2 with "allocated between", filter Detached, sort by Retained, click anHTMLLIElementand read the Retainers chain to the Map inruntime.tsx. Click "release", force GC, snapshot again: gone./js/listener-leak: the same three snapshots over remounts. The retainers path now runs through a Window event listener and a closure holding a 500,000-element array; read both.- In the console on either route,
queryObjects(HTMLLIElement).lengthbefore and after is the one-line version of the same check.
THREE SNAPSHOTS, AND THE COMPARISON VIEW
the only reliable way to tell a leak from a cache
swipe the figure sideways, or tap expand for full screen
1/6
baseline
Snapshot 1: the baseline after the page has settled (click the bin icon to force GC first; snapshots do a GC anyway). Heap 48 MB. It contains everything the page needs to exist: the framework, the store, the DOM.
19
Allocation timelines and live counters
the other Memory tools
- Allocation instrumentation on timeline: records every allocation with its stack while you act. Blue bars are allocations still alive at stop; grey were collected. One blue bar per action that never greys is a leak seen live; drag over a bar for the constructors and expand for the allocation stack (the JSX line, through source maps). Heavier than a snapshot: for a short action, not a session.
- Allocation sampling: a statistical profile of bytes allocated per function: "which code allocates most" rather than "what leaked". The tool for allocation pressure in a hot loop (the JS course part 14), paired with the Performance panel's grey GC blocks.
- Performance monitor (More tools): live graphs of CPU, JS heap, DOM nodes, listeners, documents, frames, layouts and style recalcs per second. Open it during any manual test; DOM nodes and heap that step up per action and never come down after a forced GC is a leak before any snapshot.
- The Memory checkbox in the Performance panel draws the heap sawtooth under the trace; a rising floor is growth and the flame chart above names the task that allocated.
- Targets: workers and iframes have their own heaps; the Memory panel's dropdown picks one. The Task Manager (⇧Esc) shows memory per process for first triage.
the shapes, by tool
- Detached nodes (a list kept in a Map, a node in a closure): snapshot with the Detached filter; retainers.
- Listeners never removed (window, document, a store): snapshot; retainers through the listener; the Event Listeners pane for the count.
- Subscriptions and timers without cleanup (React effects, the React course part 11): Performance monitor's listener count;
queryObjects(Component)counts of instances that should be gone. - Caches without bounds (a query cache with no gcTime, a memo Map): snapshot; the Map's retained size; not a leak by shape, a policy problem by size.
- Allocation churn (objects per frame, strings in a loop): sampling; the Performance panel's GC blocks; typed arrays and reuse.
go to the lab
/js/detached-listwith Performance monitor open: cycle ×10 and watch DOM Nodes climb by 500 per cycle; click release, force GC, watch it drop./js/worker-clone-vs-transfer: switch the Memory target to the worker and snapshot after a clone: the 50 MB ArrayBuffer is there; after a transfer, it is there and gone from the page./loop/long-task: record with Memory enabled; the sawtooth underbusyis flat (no allocation) while a chunked run shows small teeth from the promise machinery.
ALLOCATION TIMELINES AND THE PERFORMANCE MONITOR
what was allocated when, and the live counters that catch growth early
swipe the figure sideways, or tap expand for full screen
1/6
timeline
Allocation instrumentation on timeline: start, perform the action, stop. The top strip shows allocations over time as bars: blue if still alive at stop, grey if freed. A pattern of blue bars that never grey out, once per action, is a leak visible without a snapshot; drag over one bar to list the constructors and expand to the allocation stack.