Opening Heap Snapshots Too Large for DevTools
Your production process wrote a 3 GB .heapsnapshot, and loading it into Chrome DevTools either stalls at “Building dominator tree” or crashes the DevTools window — this guide from Interpreting Heap Snapshots for Memory Analysis, part of the Browser DevTools & Performance Profiling Workflows section, covers how to get answers out of snapshots that are too big to open the normal way.
| Symptom | Root Cause | Immediate Action | Measurable Impact |
|---|---|---|---|
| DevTools freezes at “Building dominator tree” | Snapshot parsing needs several times the file size in memory | Analyse in Node.js with a raised --max-old-space-size |
Loads 2–4 GB snapshots that DevTools cannot |
| DevTools tab shows “Aw, Snap!” while loading | The DevTools front-end renderer ran out of memory | Use a dedicated Chrome instance with nothing else open, or go headless | Removes competing memory use from the same machine |
| Snapshot file is larger than the heap you expected | Numeric values and internals are included | Capture with numeric values off where the API allows | Smaller files, faster load |
| Leak is obvious only at 3 GB | Reproduction runs at production scale | Reproduce the same growth with a smaller dataset or shorter uptime | 10× smaller snapshot showing the same retainer path |
| Need one fact from a huge file | Full graph load is overkill | Stream the file and aggregate counts by constructor | Answer in minutes with modest memory |
Root Cause: Snapshot Files Expand When Loaded
A .heapsnapshot file is JSON containing flat integer arrays — one for nodes, one for edges — plus a string table. On disk it is compact, but analysis needs much more: DevTools parses the JSON, builds typed arrays for nodes and edges, reconstructs retainer lists (the reverse of every edge), computes distances with a breadth-first search, then builds the dominator tree to compute retained sizes. Each of those structures is proportional to the number of nodes or edges, so the working set of the analyser can be three to five times the file size. A 3 GB snapshot can need well over 10 GB in the process doing the analysis.
DevTools performs that work inside its own front-end renderer and a worker, which are subject to the same per-process and per-heap limits as any web page. There is also a hard ceiling on how long a single JavaScript string can be in V8, which matters for any tool that reads the whole file into one string before parsing. The result is familiar: fine up to a few hundred megabytes, sluggish around 1 GB, and failing somewhere beyond that depending on machine and version.
The fix is to change where and how much you analyse. Where: Node.js lets you raise the heap limit far beyond a browser tab’s, and it can stream the file. How much: most investigations need only three facts — which constructors grew, which instances are largest, and one retainer path — and you rarely need a fully interactive graph of three billion bytes to get them. Often the best move of all is to capture a smaller snapshot that shows the same leak, because leaks grow linearly: the retainer path at 300 MB is the same path as at 3 GB. The techniques in the three-snapshot technique work just as well at small scale.
Step-by-Step Fix
- Try to shrink the reproduction first. Restart the process and capture a snapshot after one tenth of the uptime or traffic, or run the leaking flow 50 times locally instead of in production. Verification: the new snapshot is under ~500 MB and the same constructor dominates the Summary view.
- Capture smaller files at the source. In Node.js,
v8.writeHeapSnapshot()andnode --heapsnapshot-signal=SIGUSR2produce the standard format; when capturing through the DevTools Protocol, passcaptureNumericValue: falseandexposeInternals: falsetoHeapProfiler.takeHeapSnapshotso heap numbers are not recorded as separate nodes. Verification: file size drops, and constructor counts match an earlier capture. - If you must open the big file, give it a dedicated machine and browser. Close other tabs, launch a fresh Chrome instance, open DevTools on
about:blank, and use Memory → Load to open the file. Verification: the load progresses past “Building dominator tree”; if it still crashes, move to Node.js. - Aggregate in Node.js with a raised heap. Run a script such as the one below with
node --max-old-space-size=16384. It streams nothing clever — it simply has the headroom a browser renderer lacks. Verification: the script prints a constructor table sorted by total shallow size. - Extract one retainer path for the top suspect. Use Memlab’s heap analysis API, also under a large
--max-old-space-size, to print the shortest path for a sample instance. Verification: you have a written path such as(GC roots) → Module → requestCache → Map → Entry. - Confirm with a small, interactive snapshot. Reproduce locally at small scale and open that snapshot in DevTools to explore the same path interactively. Verification: the path and dominant constructor match what the large-file analysis reported.
Command and Code Reference
Use case: summarise a huge snapshot by constructor without DevTools. The raw format is regular enough to aggregate shallow size per constructor with a few typed-array passes.
// summarise.mjs — node --max-old-space-size=16384 summarise.mjs big.heapsnapshot
import { readFileSync } from 'node:fs';
// For files over ~1 GB, read as a Buffer and parse in one go; the raised
// heap limit is what makes this possible outside a browser renderer
const snap = JSON.parse(readFileSync(process.argv[2]));
const f = snap.snapshot.meta.node_fields;
const NF = f.length;
const nameOff = f.indexOf('name');
const sizeOff = f.indexOf('self_size');
const typeOff = f.indexOf('type');
const types = snap.snapshot.meta.node_types[0];
const totals = new Map(); // constructor name → [count, bytes]
for (let i = 0; i < snap.nodes.length; i += NF) {
const type = types[snap.nodes[i + typeOff]];
// group engine-internal types the same way DevTools does: "(string)", "(array)"…
const name = type === 'object' || type === 'native'
? snap.strings[snap.nodes[i + nameOff]]
: `(${type})`;
const t = totals.get(name) || [0, 0];
t[0] += 1;
t[1] += snap.nodes[i + sizeOff];
totals.set(name, t);
}
[...totals.entries()]
.sort((a, b) => b[1][1] - a[1][1])
.slice(0, 25)
.forEach(([n, [c, b]]) => console.log(`${(b / 1048576).toFixed(1).padStart(9)} MB ${String(c).padStart(10)} ${n}`));
Use case: capture a leaner snapshot from Node.js in production. The signal-based trigger avoids code changes; running it at a fraction of the usual uptime keeps the file small.
# Start the service so SIGUSR2 writes a snapshot into the working directory
node --heapsnapshot-signal=SIGUSR2 server.js &
# After ~10% of the uptime that normally shows the leak:
kill -USR2 "$(pgrep -f 'node --heapsnapshot-signal')" # writes Heap.*.heapsnapshot
ls -lh Heap.*.heapsnapshot # expect hundreds of MB, not GB
Verification and Regression Prevention
You have a trustworthy result when the large-file aggregate and a small, interactive snapshot agree: the same constructor dominates both, and the same retainer path explains it. Record both the aggregate table and the path in the incident notes so the fix can be verified later by repeating the small capture and seeing the constructor drop out of the top of the table.
Handle the file itself with care. A heap snapshot contains every string your process held — session tokens, user records, API keys read from the environment — so treat it like a database dump: move it over an encrypted channel, store it in an access-controlled bucket, and delete it when the investigation closes. Compressing it with zstd or gzip before transfer typically shrinks it by 5–10×, because the node and edge arrays are highly repetitive, which also makes it much quicker to copy off a production host.
Prevention here is about never needing the huge file again. Add a heap-growth alert that fires while the leak is still small — see alerting on memory leaks with growth slope — and wire a snapshot capture to it, so the first snapshot you get is hundreds of megabytes rather than gigabytes. In Node.js, --heapsnapshot-near-heap-limit can also write a snapshot automatically before an out-of-memory crash; pair it with a sensible heap limit so that snapshot stays analysable.
Frequently Asked Questions
Is there a maximum snapshot size DevTools can open?
There is no single documented number; it depends on the machine’s memory, the Chrome version and the shape of the graph. In practice snapshots of a few hundred megabytes open comfortably, around 1 GB becomes slow, and multi-gigabyte files frequently fail. Treat anything above 1 GB as a candidate for Node.js analysis or a smaller reproduction.
Can I split a heap snapshot into smaller files?
Not meaningfully. A snapshot is a single graph, and retained sizes and retainer paths depend on the whole graph. You can extract aggregates — counts and shallow sizes per constructor — from pieces of the node array, but dominator and path analysis need everything loaded at once.
Does a bigger snapshot mean a bigger leak?
Usually, but not only. Snapshot size reflects node and edge counts, including engine internals, so a heap with many small objects produces a larger file than one with a few big buffers of the same total size. Use the aggregate table’s byte totals, not the file size, to judge the leak.
Related
- Interpreting Heap Snapshots for Memory Analysis — the parent topic
- Writing Heap Snapshots Near the Heap Limit — automatic capture before an OOM crash
- Taking Heap Snapshots from a Live Node.js Process — capture options on servers
- Browser DevTools & Performance Profiling Workflows — the section overview