Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome back to our final module on Memory Leak Detection and Analysis.
In our last lesson, we mastered the technique of comparing heap snapshots to identify that a memory leak is occurring. By establishing a baseline, performing an action, and then comparing the memory states, we could pinpoint objects, like detached DOM nodes, that were created but not cleaned up. This answered the "what" is leaking.
Today, we address the critical next question: why is it leaking? An object is only garbage collected when no references to it exist. Our goal is to find the specific variable, closure, or event listener that is holding on to an object and preventing its disposal. To do this, we will trace object retention paths using the Containment view to find the root cause of a memory leak. The key tool for this investigation is the Retainers panel.
The Retaining Path: Finding the Root Cause
Once you've identified a suspicious object in a heap snapshot (either through the Summary or Comparison view), your job is to play detective and trace the chain of references keeping it alive. This chain is called the retaining path or retainer chain.
The JavaScript garbage collector starts at a set of "roots" (like the global window object) and traverses all object references. Anything it can reach is considered "live" and is kept. Anything it can't reach is garbage. A memory leak is simply an unwanted reference in this chain.
The Retainers panel, located at the bottom of the Memory tool, is your window into this chain. When you select any object in the upper panel, the Retainers panel shows you what is holding a reference to that object.
Let's watch a clear, practical demonstration of this process.
How To Detect Memory Leaks In Your Web App (2025)
In this clip, the presenter has already identified a leaking component from a previous analysis. Now, they'll use the Retainers panel to discover exactly why it's not being garbage collected.
Watch from 12:22 to 14:01. Observe how selecting the leaking object instance populates the 'Retainers' panel. The presenter then expands the chain of objects. Your goal isn't to understand every V8-internal object in the list, but to follow the chain until you find a familiar name or a link to your application's source code. Notice how the trail leads directly to an unhandled RxJS subscription.
As you saw, the retaining path can seem complex, often involving internal objects from the JavaScript engine or frameworks. However, the key is to look for the "weakest link" that you control. By expanding the chain, you can trace the reference from your leaked object (WeatherContentComponent in the video) back to its ultimate source. In that example, the source was a long-lived subscription that was never terminated, a very common cause of leaks in modern applications using libraries like RxJS.
The Containment View: A Top-Down Perspective
While the Retainers panel provides a bottom-up view (from the leaked object back to its root), the Containment view gives you a top-down, "bird's-eye view" of all objects in memory. It lets you explore the memory landscape starting from the GC roots.
Record heap snapshots | Chrome DevTools
The official Chrome documentation offers a concise description of the Containment view and its primary entry points.
Please read the section titled 'Containment view'. This will explain its structure and the main entry points you can use for exploration: DOMWindow objects, GC roots, and Native objects.
The Containment view is most useful when you suspect a leak might be caused by:
- Accidental globals: You can expand the
DOMWindow objectsentry to inspect everything attached to thewindowobject. If you find application-specific objects here that you didn't intend to make global, you've found a leak. - Complex closures: It allows you to peek inside function closures to see what variables they are retaining.
Here's another walkthrough that shows this in action, this time uncovering a leak caused by an un-removed event listener.
Plugging Memory Leaks With The Chrome Dev Tools - Ben Dilts
This video shows another common leak scenario. A 'game' object isn't being garbage collected after the game ends. The presenter uses the Retainers panel to trace the reference back to its source.
Watch from 11:55 to 13:44. The presenter selects the leaked 'game' object. In the 'Retainers' section, you can clearly see the path: the object is retained by a function, which is an event listener attached to the window object. This is the root cause.
This demonstrates a classic pattern:
- Identify leaking object: Find the object that shouldn't be in memory (e.g.,
game). - Select it: Click on the object in the heap snapshot.
- Inspect Retainers: Analyze the chain in the Retainers panel.
- Find the root: Trace the chain back to its origin, often a global object (
window), a long-lived service, or a static cache. The reference itself is typically an event listener, a closure, or an item in an array.

This image from the Chrome documentation illustrates a simple retainer chain. The selected string is retained by the x property of an Item instance. This is the kind of direct relationship you're looking for.
Test your understanding!
You're debugging a single-page application. After navigating away from a user profile page, you take a heap snapshot and discover a UserProfileComponent object is still in memory. You select it.
The Retainers panel shows the following chain: UserProfileComponent is retained by the context of a (closure), which is an element in a (system) / Map, which is a property named _subscribers on a NotificationService object.
What is the most likely cause of this memory leak, and what would be your first step to fix it?
Show answer
The most likely cause is that the UserProfileComponent subscribed to the NotificationService but never unsubscribed when it was destroyed. The NotificationService is a long-lived singleton, and its _subscribers map is holding a reference to a closure created within the component. That closure, in turn, holds a reference to the component instance (context), preventing it from being garbage collected.
The first step to fix this would be to implement a cleanup mechanism in UserProfileComponent (e.g., in ngOnDestroy for Angular, or useEffect's return function for React) that unsubscribes from the NotificationService.
Pro Tips for Tracing Retainers
As a senior developer, you'll appreciate a few tips that make navigating complex retainer chains much easier.
-
Name your functions: Anonymous functions and closures appear as
(closure)in a heap snapshot, which can be confusing if you have many. Giving functions names makes your snapshots much more readable.// Instead of this: button.addEventListener('click', function() { /* ... */ }); // Do this: button.addEventListener('click', function handleButtonClick() { /* ... */ }); -
Filter the Retainer chain: Complex retainer paths can sometimes have cycles or internal V8 nodes that add noise. The Microsoft Edge DevTools documentation (which is based on Chromium) points out useful filters in the Retainers panel to hide these and simplify the view. Look for a "Filter edges" dropdown in the Retainers section to hide cycles and internal nodes.
Conclusion
You now have the complete workflow for finding and fixing memory leaks. You can identify suspicious objects by comparing snapshots and then trace their retainer chains to find the exact line of code or architectural pattern that is causing the leak.
Key Takeaways:
- The Retainers panel is your primary tool for discovering why an object is being kept in memory.
- The retainer chain shows the path of references from a GC root (like the
windowobject) down to your leaked object. Your goal is to find a reference in this chain that you can break. - Common culprits for leaks include un-removed event listeners, unterminated subscriptions (e.g., from WebSockets or RxJS), and objects stored in global caches that are never cleared.
- The Containment view provides a top-down perspective of memory, which is useful for exploring objects attached to the global
windowor inspecting closures.
In the next and final lesson of this module, we will shift our focus slightly. We'll move from hunting down permanent leaks to analyzing memory allocation patterns over time. We will learn to use the Allocation Timeline and Allocation Sampling profilers to identify code that creates a lot of short-lived objects, which can cause performance issues like "jank" due to excessive garbage collection.