Chrome Memory Tab vs Performance Memory Lane

You’ve watched a page’s memory creep upward for minutes and now you’re stuck deciding whether to open the Chrome DevTools Memory tab or the Memory checkbox inside performance panel flame graph analysis — and picking wrong wastes an entire debugging session inside Browser DevTools & Performance Profiling Workflows.

Symptom Root Cause Immediate Action
Heap grows across many interactions Unclear: leak vs normal churn Record Memory checkbox in Performance
Known leak, need the retainer Timeline shows growth, not identity Take comparison snapshots in Memory tab
Growth tied to one animation Need time correlation with jank Use Performance panel, not snapshots
Object count rises per action Need retainer chain, not a trend Use Memory tab Comparison view
Suspect a hot allocation path Need per-call-site allocation data Use Allocation instrumentation

Root Cause

The confusion exists because both tools sit on top of the same V8 heap but instrument it at different layers. Mastering the Chrome DevTools Memory tab is built around discrete, on-demand captures: a heap snapshot forces a garbage collection, then walks every reachable object from the GC roots and serialises the full object graph, including constructor names, shallow size, retained size, and — critically — the chain of references keeping each object alive. That retainer chain is the only reliable way to answer “why is this object still around?” It is expensive to compute, so you take it a handful of times, not continuously.

The Performance panel’s Memory checkbox works differently. It adds a JS Heap line to the timeline by sampling usedJSHeapSize at a regular cadence throughout the recording, alongside the main-thread flame chart, network requests, and GC events (shown as purple Minor GC / Major GC bands). It has zero knowledge of object identity — it cannot tell you which array or listener is leaking — but it excels at showing when memory rises relative to specific user interactions, layout thrashing, or long tasks. A normal, healthy page produces a sawtooth: heap climbs during work, then drops sharply on each Major GC. A leaking page produces a rising staircase — each GC collects some garbage but the post-GC floor keeps climbing.

The diagram below shows the two tools side by side: one answers “when did memory start growing, and near what task?”, the other answers “what object is retained, and by what reference?” You usually need the first to detect a leak and the second to fix it.

Performance Memory Lane vs Memory Tab Heap Snapshot A two-column diagram. The left column shows the Performance panel's JS Heap line as a rising sawtooth correlated with main-thread tasks and GC markers over time, answering "when does memory grow?" The right column shows the Memory tab's snapshot comparison with a retainer chain, answering "what object is retained and by what reference?" Performance Panel Memory checkbox Memory Tab Heap snapshot Major GC markers time → JS Heap Rising floor after each GC = growth is correlated with a task or interaction Answers: "When did memory start growing, near what task?" Snapshot 1 Detached nodes: 12 Listeners: 40 Snapshot 2 Detached nodes: 212 Listeners: 340 Δ Retainer chain DetachedNode Closure scope Event listener Answers: "What object is retained, and by which reference?" then

Step-by-Step Fix

  1. Detect first, with the Performance panel. Open DevTools → Performance, tick the Memory checkbox in the toolbar, click Record, then repeat the suspect interaction 5–10 times (open/close a modal, navigate between routes). Stop recording. Expected Output: the JS Heap track appears below the flame chart; a healthy page shows a sawtooth that returns near its starting value after each Major GC band.

  2. Read the sawtooth floor, not the peaks. Zoom into the recording and compare the JS Heap value right after the first Major GC against the value right after the last Major GC in the trace. Expected Output: if the post-GC floor rose by more than a trivial amount (for example from 18 MB to 46 MB across ten repeats), you have confirmed retained memory, not just normal allocation churn.

  3. Correlate the rise with a task. Hover the JS Heap line at the point it steps up and look at the flame chart directly above it — note the function name and whether it lines up with your interaction (click handler, route change, timer callback). Expected Output: a named call frame (e.g. attachModal, onRouteChange) sitting under each step-up in the heap line.

  4. Switch to the Memory tab for identity. Open DevTools → Memory → Heap snapshot, take snapshot 1, perform the suspect interaction 3–5 times, take snapshot 2, then select snapshot 2 and set the view dropdown to Comparison. Expected Output: a sorted list of constructors with a positive # Delta — that number should roughly match your repeat count if each interaction leaks one instance.

  5. Follow the retainer chain to the fix. Expand the leaking constructor, click an instance, and read the Retainers pane at the bottom — trace upward until you hit a GC root (a global, a closure, or a detached DOM subtree). Expected Output: a concrete reference path such as Window → eventListeners → closure → DetachedHTMLDivElement, which tells you exactly which removeEventListener or cleanup call is missing.

  6. If the retainer chain is ambiguous, add allocation instrumentation. Open DevTools → Memory → Allocation instrumentation on timeline, tick “Record allocation stacks”, start recording, repeat the action once, then stop. Expected Output: blue bars in the timeline mark allocation bursts; selecting a bar filters the heap view to objects allocated in that window, each with a full JS call stack.

Command & Code Reference

A quick way to sanity-check the JS Heap counter from the console without opening either panel, useful when scripting a repro:

// Read live heap size from the Performance/Memory
// API (Chrome only, non-standard).
const mem = performance.memory;

// usedJSHeapSize: bytes currently in use.
console.log(
  'used MB:',
  (mem.usedJSHeapSize / 1048576).toFixed(1)
);

// totalJSHeapSize: bytes V8 has committed so far.
console.log(
  'total MB:',
  (mem.totalJSHeapSize / 1048576).toFixed(1)
);

// jsHeapSizeLimit: hard ceiling before OOM crash.
console.log(
  'limit MB:',
  (mem.jsHeapSizeLimit / 1048576).toFixed(1)
);

A minimal harness to poll and log heap growth every second while you drive the repro manually, so you can eyeball a rising floor without recording a full Performance trace:

// Sample used heap once per second and warn if
// it keeps climbing across 5 consecutive samples.
let risingCount = 0;
let lastUsed = performance.memory.usedJSHeapSize;

setInterval(() => {
  // Force a rough GC hint by yielding a tick first.
  const used = performance.memory.usedJSHeapSize;
  const deltaMB = (used - lastUsed) / 1048576;

  if (deltaMB > 0.5) {
    // Heap grew meaningfully since last sample.
    risingCount++;
  } else {
    risingCount = 0;
  }

  if (risingCount >= 5) {
    // Five straight rises: likely a real leak.
    console.warn('Heap rising for 5s straight:',
      (used / 1048576).toFixed(1), 'MB');
  }

  lastUsed = used;
}, 1000);

Verification & Regression Prevention

Set explicit thresholds so you don’t rely on eyeballing the sawtooth every time:

  • Post-GC floor delta: capture usedJSHeapSize immediately after a forced Major GC before and after N repeats of the interaction; fail the check if the delta exceeds roughly 1 MB per repeat (tune per feature).
  • Detached node count: in a Memory tab Comparison snapshot, the # Delta for Detached HTMLDivElement (or your framework’s root node type) should return to 0 after the interaction completes and any associated component unmounts.
  • CI hook: wire a headless Puppeteer script that drives the interaction 20 times, reads performance.memory.usedJSHeapSize before and after via page.metrics(), and fails the build if growth exceeds a fixed MB budget — run it in the same job that already runs profiling SPAs in DevTools checks.
  • Monitoring threshold: in production RUM, alert when a session’s usedJSHeapSize grows past a fixed ceiling (e.g. 150 MB) without a corresponding Major GC drop within 60 seconds, which mirrors the same staircase pattern you saw in the trace.

Frequently Asked Questions

Can I use the Performance panel Memory checkbox instead of taking heap snapshots?

Only for detection, not diagnosis. The Memory checkbox in the Performance panel tells you that memory is growing and roughly when, correlated with tasks and GC events, but it has no concept of object identity or retainer chains. Once you’ve confirmed a leak exists, you still need the Memory tab’s heap snapshots to find which objects are retained and why.

Why does the JS Heap line in the Performance panel not match the Memory tab’s snapshot size?

The JS Heap line in the Performance panel samples total V8 heap usage continuously, including short-lived garbage that hasn’t been collected yet, while a heap snapshot forces a garbage collection before serializing the graph and only counts reachable objects. It is normal for the snapshot number to be lower than the nearest point on the timeline.

Does recording the Memory lane in the Performance panel slow down my page like a CPU profile?

The Memory checkbox adds a lightweight sampling overhead that is far cheaper than a full heap snapshot or allocation timeline, so it’s safe to leave on during routine performance recordings. Heap snapshots and allocation instrumentation are heavier because they walk or hook every allocation, and both pause the page noticeably longer.