Reading retainers, detached nodes and allocation timelines
A single heap snapshot shows only one moment in time, but the three snapshot technique systematically exposes which objects stay in memory despite the garbage collector. Retainer paths show exactly which reference prevents release, turning a vague memory suspicion into a concrete cause in the code.
Table of Contents
- 1. Why a single heap snapshot is not enough
- 2. Anatomy of a heap snapshot: objects, edges and sizes
- 3. Reading retainer paths: why an object is not released
- 4. The three snapshot technique in detail
- 5. Finding and understanding detached DOM nodes
- 6. Closures and event listeners as a common leak source
- 7. Allocation timeline: tracking allocations over time
- 8. Practical workflow for reproducible analyses
- 9. Memory panel tools compared
- 10. Summary
- 11. FAQ
1. Why a single heap snapshot is not enough
A heap snapshot is a complete snapshot of every object in the JavaScript heap at a given moment, including their size and their relationships to each other. A single heap snapshot shows which objects currently exist and how much memory they occupy, but reveals nothing about whether these objects actually represent a memory leak or are simply normal, active application state. Without a comparison point, a single heap snapshot has little diagnostic value for leak detection.
A memory leak in JavaScript always arises when an object that is no longer actually needed remains reachable through an active reference chain and therefore cannot be collected by the garbage collector. A heap snapshot alone does show these reference chains, but only comparing multiple heap snapshots over time reveals which objects continuously accumulate instead of being released after use.
Experience shows that isolated heap snapshots often lead to misinterpretation: a large heap snapshot can simply mean the application is currently actively processing a lot of data, without a single object actually being leaked. Only the systematic method with multiple heap snapshots, especially the three snapshot technique presented in the next section, reliably separates normal memory growth from real memory leaks.
2. Anatomy of a heap snapshot: objects, edges and sizes
Technically, a heap snapshot consists of a directed graph: nodes represent objects in the heap, edges represent references between these objects. Each node carries information about its type, for example whether it is a plain object, an array, a closure or a DOM node, as well as its shallow size, the memory the object itself directly occupies, excluding the objects it references.
The retained size of an object in the heap snapshot is the more important metric for leak analysis: it indicates how much memory in total would be freed if this one object were removed from the heap, including all objects reachable exclusively through this one object. An object with a small shallow size but a huge retained size is often the actual root of a memory problem, because it keeps an entire tree of otherwise unreachable objects alive.
The Chrome DevTools Memory Panel offers three views on a heap snapshot: Summary groups objects by constructor name and shows total sizes per type, Comparison compares two heap snapshots and shows deltas, and Containment shows the raw object structure starting from the root objects. For most leak analyses, Comparison is the most valuable view, because it directly shows which object types have increased between two points in time.
// Programmatically triggering a heap snapshot via Chrome DevTools Protocol
// Useful for automated memory regression tests in CI
import CDP from "chrome-remote-interface";
async function captureHeapSnapshot(port) {
const client = await CDP({ port });
const { HeapProfiler } = client;
await HeapProfiler.enable();
let snapshotData = "";
HeapProfiler.addHeapSnapshotChunk(({ chunk }) => {
snapshotData += chunk;
});
await HeapProfiler.takeHeapSnapshot({ reportProgress: false });
await client.close();
return snapshotData; // JSON heap snapshot, can be loaded into DevTools
}
3. Reading retainer paths: why an object is not released
The retainer path of an object in a heap snapshot shows the chain of references that keeps this object alive against garbage collector release, starting from a root object like window or a global context. In the Memory Panel this path can be expanded by clicking an object in the lower half of the window, with each link of the chain as its own further expandable entry.
Reading retainer paths follows a fixed pattern: from bottom to top the path shows which specific object, for example a variable in a closure or an entry in an array, holds the reference. Frequently, the top of the retainer path leads to a globally registered event listener, a timer that was never cleared, or a Map or Set that entries were added to but never removed from.
An important tip when reading retainer paths in a heap snapshot: the label in square brackets, for example [[Scopes]] or a property name, shows the type of reference. An entry named context usually points to a closure holding onto an outer variable. This distinction helps quickly tell apart different leak patterns without having to manually search through the entire codebase.
4. The three snapshot technique in detail
The three snapshot technique is the most reliable method for distinguishing real memory leaks from normal memory behavior. The process: first, a baseline heap snapshot is created once the application is in a stable idle state. Then the suspicious action, for example opening and closing a modal or navigating between two routes, is repeated several times, followed by a second heap snapshot.
After the second heap snapshot, the same action is repeated several more times, and a third heap snapshot is taken. The decisive step now is the comparison: objects that increase between the first and second heap snapshot and keep increasing continuously between the second and third are, with high probability, a real memory leak. Objects that increase only once and then stay stable are usually normal, expected memory behavior of the application.
This method works so reliably because a real memory leak, by definition, keeps accumulating with every repetition of the triggering action, while one time memory growth, for example due to caching or lazy initialization, stays constant after the first occurrence. The Memory Panel directly offers the option to display only objects newly added between two selected snapshots, which considerably simplifies the three snapshot technique.
// Automating the three-snapshot technique with Puppeteer
async function detectMemoryLeak(page, triggerAction, iterations = 5) {
const session = await page.target().createCDPSession();
await session.send("HeapProfiler.enable");
async function takeSnapshot() {
let data = "";
session.on("HeapProfiler.addHeapSnapshotChunk", (e) => (data += e.chunk));
await session.send("HeapProfiler.takeHeapSnapshot");
return data;
}
await triggerAction(page); // warm up, e.g. open/close a modal once
const baseline = await takeSnapshot();
for (let i = 0; i < iterations; i++) await triggerAction(page);
const second = await takeSnapshot();
for (let i = 0; i < iterations; i++) await triggerAction(page);
const third = await takeSnapshot();
// Compare object counts by constructor between second and third snapshot
// Objects growing consistently across both deltas indicate a real leak
return { baseline, second, third };
}
5. Finding and understanding detached DOM nodes
A detached DOM node is a DOM element that has been removed from the visible document tree but is still referenced by JavaScript code and therefore cannot be collected by the garbage collector. This situation typically arises when an event listener or a variable holds a reference to an element that was removed via removeChild or by replacing innerHTML, without the reference itself being cleaned up.
In a heap snapshot, detached DOM nodes can be specifically found via the filter function in the Summary view, by filtering for Detached. The Memory Panel then shows all DOM elements that still exist in memory but no longer have any connection to the active document. This list is often the fastest way to identify DOM related memory leaks, especially in single page applications where components are frequently mounted and removed.
A particularly tricky pattern with detached DOM nodes arises when an entire detached subtree hangs off a single element that is itself referenced by a closure. In this case, the heap snapshot may show only a single, small looking detached element, whose retained size is enormously inflated by hundreds of child elements. The retainer path of this one root element then leads directly to the causing code location.
6. Closures and event listeners as a common leak source
Closures are one of the most common causes of memory leaks in JavaScript, because by definition they hold references to their surrounding scope, even if only a single variable from that scope is actually needed. A function registered as an event listener that accidentally references a large data structure from its surrounding scope keeps that data structure in memory for as long as the event listener remains registered, even if the data is no longer needed at all.
An especially common pattern: an event listener is registered again on every component instantiation, but never removed when the component is torn down. In frameworks with lifecycle hooks, that means every creation and destruction of a component accumulates another event listener, each with its own closure and the references trapped within it. The heap snapshot shows this pattern as a continuously growing count of function objects with the same name.
The most reliable countermeasure is pairing every addEventListener call with a corresponding removeEventListener call in the cleanup path, ideally with an AbortController that removes multiple listeners in one bundled operation. The same principle applies to timers: every setInterval call needs a corresponding clearInterval, otherwise the timer keeps its closure and all objects referenced within it alive permanently.
7. Allocation timeline: tracking allocations over time
The allocation timeline in the Memory Panel records memory allocations over a period of time and displays them as a bar chart, where each blue bar represents a group of allocations at a specific moment. Unlike a single heap snapshot, which only shows the final state, the allocation timeline makes visible exactly when in the timeline memory was allocated, which considerably eases attribution to a specific user action.
A particularly useful feature of the allocation timeline is the ability to select a specific time range with the mouse and display only the objects allocated within exactly that range that are still present in memory at the end of the recording. Objects that were allocated but already released before the end of the recording appear in the chart as lighter, grayed out bars, visually separating normal memory behavior from potentially problematic behavior.
The allocation timeline is especially suited for investigating recurring user actions like scrolling or filtering, where it is unclear whether each repetition creates new objects that are correctly released after use, or whether additional memory accumulates with each repetition. Together with the three snapshot technique, the allocation timeline provides a complete picture of both the final state and the timeline of a possible memory leak.
8. Practical workflow for reproducible analyses
A reproducible workflow for heap snapshot analyses always starts with a manual garbage collection via the trash can button in the Memory Panel, before even taking a snapshot. Without this step, objects not yet collected but already no longer referenced can distort the snapshot and create the impression of a leak where the garbage collector has simply not run yet.
For consistent results, the suspicious action should always be triggered via the same interaction path, ideally automated via a script rather than manually with the mouse, to eliminate human variation in timing and order. Combining programmatic control via the Chrome DevTools Protocol with the three heap snapshots of the three snapshot technique delivers reproducible memory regression tests that can be integrated into CI pipelines, catching new leaks before they reach production.
9. Memory panel tools compared
The Memory Panel offers several tools for different questions. The table below classifies which tool is best suited for which kind of memory problem.
| Tool | Shows | Strength | Limit |
|---|---|---|---|
| Heap snapshot | Full object graph at one point in time | Retainer paths, exact cause | Only a snapshot, no timeline |
| Three snapshot technique | Delta across multiple repetitions | Separates real leaks from normal growth | Requires several manual steps |
| Allocation timeline | Allocations over time | Attribution to actions over time | No retainer path directly in the chart |
| Allocation sampling | Statistical distribution by function | Low overhead, long recordings | No object identity, only aggregates |
In practice these tools complement each other. Allocation sampling suits an initial, long running overview with low overhead, the allocation timeline then shows the time context of a suspicious range, and the three snapshot technique with subsequent retainer path analysis ultimately delivers the exact line of code responsible for the leak.
Mironsoft
JavaScript memory profiling and memory leak diagnosis
Systematically tracking down memory leaks?
We analyze your application with heap snapshots and the three snapshot technique, find retainer paths and detached DOM nodes, and deliver concrete fixes instead of guesswork.
Leak diagnosis
Apply the three snapshot technique, trace retainer paths to the cause
Fix detached nodes
Clean up event listeners and closures, apply AbortController
CI regression tests
Integrate automated heap snapshot comparisons into pipelines
10. Summary
A single heap snapshot only shows one moment in time and is not sufficient for solid leak diagnosis. The three snapshot technique reliably separates real memory leaks from normal, one time memory growth, by checking whether objects keep increasing continuously with every repetition of an action. Retainer paths then show exactly which reference chain keeps an object alive against garbage collector release, usually via closures, forgotten event listeners or timers that were never cleared.
Detached DOM nodes can be specifically found via the filter function in the Memory Panel, and the allocation timeline adds the time dimension to the analysis that a plain heap snapshot cannot provide. Combining these tools and keeping the workflow consistent with manual garbage collection before every snapshot turns vague memory suspicions into concrete, reproducible diagnoses with a clear line of code as the cause.
Heap Snapshots and Memory Profiling — The Essentials at a Glance
Three snapshot technique
Baseline, then repeat the action, second snapshot, repeat again, compare against the third snapshot.
Retainer paths
Show the reference chain from the root object to the unreleased object, usually via closures or listeners.
Detached DOM nodes
Specifically findable via the Detached filter in the Summary view, often entire subtrees hanging off one root element.
Allocation timeline
Shows allocations over time, grayed out bars mark objects already released again.