Heap Snapshots and Comparison
Your X-Ray Machine for Memory
A heap snapshot is a complete photograph of every object alive in your JavaScript heap at one instant. Every string, every array, every DOM node, every closure — all frozen in time with their sizes and references.
If you have ever stared at a creeping memory graph and wondered "where is it all going?", heap snapshots are the answer. They do not sample. They do not estimate. They show you everything.
Think of a heap snapshot like a census of a city. At the moment you take the snapshot, every resident (object) is counted, categorized (constructor name), measured (size in bytes), and their connections mapped (who references whom). You can then compare two censuses to see who moved in, who moved out, and who is suspiciously still hanging around.
Taking a Heap Snapshot
- Open Chrome DevTools → Memory tab
- Select "Heap snapshot" (the default radio button)
- Click "Take snapshot"
That is it. Chrome pauses your page briefly, walks the entire heap, and builds a snapshot. The snapshot shows up in the left sidebar with its total size.
Taking a snapshot triggers a full garbage collection first. This means you are only seeing objects that cannot be collected — truly live objects. No false positives from pending GC.
The Four Views
Every heap snapshot can be viewed through four lenses. Each answers a different question.
| View | Question It Answers | When to Use |
|---|---|---|
| Summary | What types of objects are using the most memory? | Starting point — get the big picture of memory distribution |
| Comparison | What changed between two snapshots? | Leak hunting — find objects that were allocated but never freed |
| Containment | What is the full reference tree from GC roots? | Understanding ownership — who owns what |
| Statistics | How is memory split across categories? | Quick overview — code vs strings vs arrays vs typed arrays |
Summary View
The Summary view groups objects by their constructor name (the function that created them). Each row shows:
- Constructor — the object type (
Array,HTMLDivElement,Object, your class names) - Distance — shortest path from any GC root (lower = more directly reachable)
- Shallow Size — memory consumed by the object itself (not what it points to)
- Retained Size — memory that would be freed if this object were garbage collected
Sort by Retained Size to find the biggest memory consumers. This is your starting point for most investigations.
Comparison View
This is the leak hunter's best friend. Select two snapshots, and the Comparison view shows:
- New — objects allocated since the previous snapshot
- Deleted — objects freed since the previous snapshot
- Delta — net change (New minus Deleted)
- Size Delta — net memory change
If you navigate between two pages and the delta is positive (more objects were created than destroyed), you likely have a leak.
The Three-Snapshot Technique
This is the gold standard for confirming memory leaks:
Compare Snapshot 3 against Snapshot 1. In a leak-free app, they should be nearly identical. Any significant delta between them points to objects that were allocated during the action but not freed when you undid it.
Containment View
The Containment view shows the heap as a tree from GC roots down. It answers: "What is the chain of ownership from the root to this object?"
The top-level entries are GC roots:
- Window / Global — the
windowobject and everything reachable from it - GC roots (internal) — V8 internal roots (compiled code, handles)
- Pending activities — timers, promises, observers
Expanding any node shows what it directly references. This is useful when you want to understand the structure of memory ownership, not just sizes.
Statistics View
The simplest view — a pie chart showing memory distribution across categories:
- Code — compiled JavaScript
- Strings — string data
- JS arrays — array backing stores
- Typed arrays —
ArrayBuffer,Float64Array, etc. - System objects — V8 internal objects
This is useful for quick diagnosis. If "Strings" is 80% of your heap, you know where to look. If "Typed arrays" is dominating, you might have image or audio buffer leaks.
Filtering by Constructor
In the Summary view, you can filter by constructor name. This is powerful when you know what type of object is leaking:
- Type the constructor name in the class filter box
- The view filters to show only objects of that type
- Click any instance to see its retaining path
Common filters for leak hunting:
Detached— finds detached DOM trees- Your component class names — finds leaked component instances
Array— finds unexpectedly large or numerous arraysClosure— finds closures that should have been collected
Objects created by object literals ({}) show up as constructor Object. Objects created with new MyClass() show the class name. Arrow functions and anonymous functions may show up as (closure) or with auto-generated internal names. If you use factory functions instead of classes, your objects will all be Object, making them harder to filter. Consider using classes or adding a constructor property when debugging-friendliness matters.
Practical Workflow: Finding a Leak
Let us walk through a complete leak-hunting session:
Understanding Object Categories
When you expand a constructor in the Summary view, you will see several system-level categories:
- (compiled code) — JIT-compiled machine code for functions
- (system) — V8 internal objects (hidden classes, maps, etc.)
- (string) — string data stored in the heap
- (array) — array backing stores
- (concatenated string) — strings created by concatenation (stored as trees of references to avoid copying)
- (sliced string) — substrings that reference the parent string
Concatenated strings and memory
When you concatenate strings with +, V8 does not immediately create a new flat string. Instead, it creates a ConsString (concatenated string) that holds references to both operands. This is fast for creation but means the original strings stay alive. If you build a huge string by concatenating thousands of small strings in a loop, you get a deep tree of ConsString nodes, each holding the previous result. Calling a method like .indexOf() on the result forces V8 to "flatten" it into a single contiguous string. This is why string concatenation in tight loops can cause unexpected memory spikes — use array .join() instead.
- 1Always force GC (trash can icon) before taking comparison snapshots — otherwise you get false positives
- 2Sort Summary view by Retained Size to find the biggest memory consumers
- 3Use the three-snapshot technique (baseline, action, undo+GC) to confirm leaks definitively
- 4Filter by 'Detached' to find DOM trees that were removed but are still referenced
- 5Click individual instances to see retaining paths — this is how you find the actual code causing the leak
| What developers do | What they should do |
|---|---|
| Taking snapshots without forcing GC first Without forced GC, objects that are eligible for collection appear as leaks in your comparison | Always click the trash can icon before taking a comparison snapshot |
| Only looking at shallow size Shallow size is just the object itself. Retained size includes everything it keeps alive | Sort by retained size — a tiny object can retain gigabytes |
| Comparing two snapshots taken at different app states If the app state changed, new objects are expected — you need to return to baseline to identify true leaks | Use three-snapshot: baseline → action → undo → compare with baseline |
| Giving up when all leaked objects show as generic 'Object' The constructor name is just a starting point — the retaining path reveals the actual source of the leak | Click instances to examine properties and retaining paths |