Skip to content

Heap Snapshots and Comparison

advanced16 min read

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.

Mental Model

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

  1. Open Chrome DevTools → Memory tab
  2. Select "Heap snapshot" (the default radio button)
  3. 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.

Info

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.

ViewQuestion It AnswersWhen to Use
SummaryWhat types of objects are using the most memory?Starting point — get the big picture of memory distribution
ComparisonWhat changed between two snapshots?Leak hunting — find objects that were allocated but never freed
ContainmentWhat is the full reference tree from GC roots?Understanding ownership — who owns what
StatisticsHow 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.

Quiz
In the Summary view, you see 10,000 instances of a constructor called 'ListItem' with 2MB total shallow size but 200MB retained size. What does this tell you?

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:

Execution Trace
Snapshot 1
Baseline — app in initial state
Establishes the baseline memory footprint
Action
Perform the suspected leaky action
Navigate to a page, open a modal, trigger a feature
Snapshot 2
After action — new objects allocated
Shows what was created by the action
Undo
Reverse the action (navigate back, close modal)
Objects from the action should now be eligible for GC
Force GC
Click trash can icon to trigger collection
Ensures all collectible objects are actually freed
Snapshot 3
After undo + GC
Compare with Snapshot 1 — any delta is a leak

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.

Quiz
You use the three-snapshot technique: Snapshot 1 (baseline), perform action, Snapshot 2, undo action, force GC, Snapshot 3. Snapshot 3 has 500 more HTMLDivElement instances than Snapshot 1. What is the most likely conclusion?

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 window object 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 arraysArrayBuffer, 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:

  1. Type the constructor name in the class filter box
  2. The view filters to show only objects of that type
  3. 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 arrays
  • Closure — finds closures that should have been collected
Common Trap

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:

Quiz
You are hunting a leak. In the Comparison view, you see +2,000 instances of constructor 'Object' with a total size delta of +50MB. Most constructor names are just 'Object'. What is the best next step?

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.

Key Rules
  1. 1Always force GC (trash can icon) before taking comparison snapshots — otherwise you get false positives
  2. 2Sort Summary view by Retained Size to find the biggest memory consumers
  3. 3Use the three-snapshot technique (baseline, action, undo+GC) to confirm leaks definitively
  4. 4Filter by 'Detached' to find DOM trees that were removed but are still referenced
  5. 5Click individual instances to see retaining paths — this is how you find the actual code causing the leak
What developers doWhat 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