Skip to content

Memory Tab: Heap Snapshots

advanced15 min read

Memory Problems Are Invisible Until They're Catastrophic

Here's what makes memory leaks so nasty: they don't throw errors. They don't show up in the Console. Your app works perfectly for 5 minutes, gets sluggish at 10, and crashes at 30. By the time a user reports it, the only symptom is "the app got slow." Without heap snapshots, you're stuck staring at code and hoping to spot the problem. That's not a strategy.

Mental Model

A heap snapshot is a photograph of everything alive in memory at the moment you click the button. Every JavaScript object, every DOM node, every string, every closure — captured with its size and every reference keeping it alive. Taking two photographs — before and after an action — and comparing them reveals exactly what was created and not cleaned up. It's a diff of the entire heap.

Three Profiling Modes

The Memory panel gives you three distinct tools — and picking the right one upfront saves you from going down the wrong rabbit hole.

1. Heap Snapshot

Question it answers: "What's in memory right now, and why is it there?"

Takes a complete snapshot of the JavaScript heap. You see every object, its size, and the chain of references keeping it alive (the retainer tree). Best for comparing two points in time to find leaks.

2. Allocation Instrumentation on Timeline

Question it answers: "Which objects are being allocated over time, and which survive garbage collection?"

Records a timeline of allocations. Blue bars show objects allocated at that moment. Bars that remain blue were not collected — potential leaks. Bars that disappear were properly GC'd. Best for finding when during a workflow leaks happen.

3. Allocation Sampling

Question it answers: "Which functions are allocating the most memory?"

Low-overhead sampling profiler that attributes memory allocations to the functions that created them. Think of it as a memory-focused flame graph. Best for finding allocation hot spots in production-like scenarios where full instrumentation is too slow.

Quiz
You suspect a memory leak occurs when users repeatedly open and close a modal. Which Memory panel mode is best for confirming this?

Reading the Summary View

You took a snapshot. Now you're staring at a table with thousands of rows. Don't worry — once you know what the columns mean, it clicks fast. The default Summary view groups objects by constructor name:

ColumnMeaning
ConstructorThe function/class that created these objects (Array, Object, HTMLDivElement, your components)
DistanceShortest path from the GC root to the object. Distance 1 = directly referenced by a root
Shallow SizeMemory used by the object itself (its own fields/properties)
Retained SizeMemory that would be freed if this object were garbage collected — includes all objects only reachable through it
Shallow Size vs Retained Size

Shallow Size is the object's own bytes. Retained Size is the object plus everything it exclusively keeps alive. A 100-byte object that holds the only reference to a 10MB buffer has 100 bytes Shallow Size but ~10MB Retained Size. Always sort by Retained Size when hunting leaks — it shows the true memory impact.

Spotting Anomalies

  • Unexpectedly high counts: 50,000 instances of EventListener? That's a leak.
  • Large Retained Size with small Shallow Size: The object itself is small but keeps a massive subgraph alive — a closure holding a reference to an entire component tree.
  • Detached DOM trees: Appear under Detached HTMLDivElement (or similar). These are DOM nodes removed from the document but still referenced by JavaScript.
Quiz
A heap snapshot shows an object with 200 bytes Shallow Size and 45 MB Retained Size. What does this tell you?

The Comparison View: Finding Leaks Systematically

This is where it gets good. The Comparison view is the single most powerful technique for memory leak detection, and once you learn it, you'll wonder how you ever debugged leaks without it:

  1. Take Snapshot 1 — This is your baseline. The app is in a clean state.
  2. Perform the suspect action — Open a modal, navigate to a page, load data. Then undo it — close the modal, navigate back, clear the data.
  3. Force garbage collection — Click the trash can icon in the Memory panel. This ensures anything that can be collected is collected before the next snapshot.
  4. Take Snapshot 2 — Select Snapshot 2, then change the view dropdown from "Summary" to "Comparison" and compare against Snapshot 1.

The Comparison view shows:

ColumnMeaning
# NewObjects of this type created between snapshots
# DeletedObjects of this type collected between snapshots
# DeltaNet change (New - Deleted). Positive delta = potential leak
Size DeltaNet memory change for this type

Sort by # Delta descending. Objects with positive deltas after you've undone the action are still alive when they shouldn't be.

Quiz
You take Snapshot 1, open a settings panel, close it, force GC, then take Snapshot 2. The Comparison shows +200 EventListener objects and +15 Detached HTMLDivElement. What's the likely problem?

The Retainer Tree: Finding Why Objects Stay Alive

You found a leaked object. Now the real question: why is it still alive? Click on it, and the bottom panel shows the Retainers — the chain of references from the GC root to this object. This is your smoking gun.

Object @123456 (Detached HTMLDivElement)
  ← tooltipRef in Object @789012
    ← scopedVars in functionContext @345678
      ← handleClick in Object @901234
        ← listeners in EventTarget @567890
          ← (GC root: Window)

Read this bottom-to-top: the Window holds an EventTarget, which has a listeners map, which contains a handleClick closure, which captured a scope that includes a tooltipRef variable pointing to the detached DOM node. The fix is to break any link in this chain — remove the event listener, null out the ref, or use a WeakRef.

Understanding GC roots

Objects stay alive because they're reachable from a GC root. In the browser, GC roots include:

  • The global object (window/globalThis)
  • The currently executing call stack (local variables in running functions)
  • Active timers (setInterval/setTimeout callbacks and their closures)
  • Active event listeners (handlers attached to live DOM nodes or global targets)
  • Active Promises (pending promise chains)
  • Active observers (IntersectionObserver, MutationObserver, ResizeObserver callbacks)

If an object is reachable from any root through any chain of references, it cannot be garbage collected. Memory leaks happen when an object is reachable from a root but you no longer need it — the code forgot to break the chain.

Identifying Detached DOM Trees

This is the part that burns people in SPAs. Detached DOM trees are one of the most common memory leaks, and they're sneaky. They happen when JavaScript removes a DOM node from the document but holds onto a reference to it — the node is gone from the page, but very much alive in memory.

In the heap snapshot, type "Detached" in the filter box. All detached DOM nodes appear. Click one to see its retainer chain — this tells you exactly which variable is holding the reference.

Common causes:

  • React refs to unmounted componentsuseRef value pointing to a DOM node after the component unmounts
  • Cached DOM queriesconst el = document.querySelector('.tooltip') stored in a module-level variable
  • Event listeners on removed elements — The listener closure captures the element
Common Trap

A single leaked DOM node can retain an entire detached tree. If a leaked JavaScript reference points to one <div> that was a child of a large component tree, the entire tree stays in memory because DOM nodes reference their parents and children. One forgotten ref can hold megabytes of detached DOM.

The Three Snapshot Technique

Two snapshots can lie to you. Here's why: the first time you perform an action, your app might legitimately create persistent objects (caches warming up, lazy modules loading). Those look like leaks but aren't. Three snapshots give you the truth:

  1. Snapshot 1: Baseline after app loads
  2. Perform the suspect action once (open/close a panel). Snapshot 2.
  3. Perform the same action again. Snapshot 3.

Compare Snapshot 3 against Snapshot 2. Objects that appear in Snapshot 2 vs 1 might be legitimate one-time allocations (caches, lazy initialization). But objects that appear again in Snapshot 3 vs 2 are leaked per-action — every repetition leaks more.

Quiz
Why use three snapshots instead of two when hunting memory leaks?

Allocation Timeline for Hot Spots

Sometimes you need to see allocations happening in real time rather than comparing static snapshots. The Allocation Instrumentation timeline shows a blue bar for each allocation. Over time, bars that survive GC stay blue; bars that are collected disappear. Super useful for pinpointing exactly when during a user flow things start leaking.

Steps:

  1. Start Allocation Instrumentation recording
  2. Perform the workflow slowly
  3. Stop recording
  4. Click on blue bars (surviving allocations) to see what objects were created at that moment
  5. Correlate timing with user actions to pinpoint which action leaks
Quiz
In the Allocation Instrumentation timeline, what do persistent blue bars indicate?
Key Rules
  1. 1Heap Snapshot = photograph of the entire heap. Comparison view between two snapshots is the primary leak detection tool.
  2. 2Always sort by Retained Size, not Shallow Size. A small object can exclusively retain gigabytes.
  3. 3The Retainer tree shows the chain of references keeping a leaked object alive — break any link to fix the leak.
  4. 4Detached DOM trees are the #1 SPA memory leak. Filter by 'Detached' in heap snapshots to find them.
  5. 5The three-snapshot technique separates one-time initialization costs from per-action leaks.
  6. 6Force GC (trash can icon) before taking comparison snapshots to avoid false positives from pending collection.
  7. 7Allocation Instrumentation shows when objects are allocated over time. Persistent blue bars that outlive their action are leaks.
Interview Question

Q: A user reports that your SPA gets progressively slower over 30 minutes of use. Walk me through how you'd investigate with the Memory panel.

A strong answer: (1) Open Task Manager (Shift+Esc in Chrome) to confirm the tab's memory is growing over time — this confirms a leak exists. (2) Use the three-snapshot technique: Snapshot 1 (baseline), perform a common user workflow (navigate, interact, navigate back), Snapshot 2, repeat the same workflow, Snapshot 3. (3) Compare Snapshot 3 against Snapshot 2 in Comparison view. Sort by # Delta. (4) Objects with positive delta are leaking per cycle. Click on one to inspect the Retainer tree. (5) Follow the retainer chain from the GC root to the leaked object — this reveals the exact reference preventing collection. (6) Common fixes: add cleanup to useEffect return, use AbortController for event listeners, replace strong references with WeakRef or WeakMap for caches, null out refs on unmount.