Does your app slow down after hours of use? Here is how to debug a memory leak: compare heap snapshots you take before and after a suspect action, then follow the retainer path to the culprit.
This page is one spoke of the wider guide to debugging techniques on this site.
| Item | Value |
|---|---|
| Profile types in the Chrome Memory panel | Four profile types |
| Shortcut to repeat a heap snapshot | Cmd + E (macOS) or Ctrl + E (Windows, Linux) |
| Garbage collection when a snapshot is taken | Always runs first |
| Performance overhead of allocation sampling | Minimal |
These figures come from Chrome’s Memory panel overview and its heap snapshot guide. Documentation changes, so check the sources before you rely on them.
What a memory leak looks like in practice
A memory leak happens when your app keeps references to objects it no longer needs, so the garbage collector cannot reclaim them. According to OpenReplay’s debugging guide, a leak shows continuously growing memory consumption that never plateaus, even when the workload stays constant. High but stable usage is a different problem and calls for a different fix.
The same guide lists the usual warning signs:
- Memory usage climbs steadily over time without dropping.
- Performance degrades after extended use.
- Browser tabs become unresponsive or crash.
Before you open a profiler, repeat the suspect action several times and watch whether memory returns to a baseline. A heap that settles is usually healthy. A heap that ratchets upward after every cycle is your lead.
How to take and compare heap snapshots
The heap snapshot is the workhorse of leak hunting. Chrome describes it as showing memory distribution among your page’s JavaScript objects and related DOM nodes, as laid out in its heap snapshot guide. Taking a snapshot always starts with garbage collection, so what you see is memory that is still reachable.
Follow this sequence:
- Open DevTools on the page you want to profile and go to the Memory panel.
- Select the Heap snapshot type, choose a JavaScript VM instance and click Take snapshot.
- Run the suspect action in your app, such as opening and closing a dialog.
- Click the trash can icon to force garbage collection, then take another snapshot.
- Select the later snapshot, switch to the Comparison view and look for objects with positive Delta values.
- Click a growing object and read the Retainers panel to see what holds it.
Once you have chosen the type and instance, you can press Cmd + E on macOS or Ctrl + E on Windows and Linux to take snapshots repeatedly. Objects that consistently increase between snapshots indicate potential leaks, and the Retained Size column shows how much memory would be freed if that object were removed.

The retainer path is the payoff. It shows why an object stays in memory, so you can follow the chain from a garbage collection root to the reference you need to break.
Choosing the right Memory panel profile
Snapshots are not the only option. Chrome’s Memory panel overview says the panel lets you capture four profile types, each with a different perspective:
- Heap snapshot: memory distribution among JavaScript objects and related DOM nodes at one moment.
- Allocation instrumentation on timeline: instrumented allocations over time, which helps you isolate leaks during a specific interaction.
- Allocation sampling: allocations by JavaScript execution stack, with minimal performance overhead, so it suits long-running operations.
- Detached elements: objects retained by a JavaScript reference.
Start with snapshots to prove growth exists. Switch to the timeline when you need to tie growth to one user action, and to sampling when the leak only appears after a long run.
One caveat: snapshots show only the objects in the memory graph that are reachable from the global object. If something is held by a native resource outside that graph, a snapshot may under-report it.
Common leak patterns and how to fix them
Most leaks fall into a handful of shapes. Learn them and you can often guess the retainer before you open the snapshot.
Detached DOM nodes
Elements removed from the document but still referenced in JavaScript stay in memory. OpenReplay suggests searching for “Detached” in the heap snapshot filter to find these orphaned nodes. The fix is to clear the reference, for example by setting the variable that holds the element to null after you remove it.
Event listeners that are never removed
Event listeners that are not removed when components unmount accumulate over time. In a React effect, return a cleanup function that calls removeEventListener. Vue, Angular and other frameworks have equivalent lifecycle hooks for tearing down listeners and subscriptions.
Closures holding large data
Closures keep parent scope variables alive, potentially retaining large objects unnecessarily. If a long-lived callback only needs a small value, copy that value out instead of capturing the whole scope.
Unbounded containers and caches
The Chromium project’s memory debugging notes state that the most common cause is adding elements to a container with no limit. Code often assumes it will be called rarely, or the code that clears the container runs less often than expected. Put a size limit or an eviction rule on every cache and queue.
For caches keyed by objects, OpenReplay recommends replacing a regular Map with a WeakMap so entries can be garbage collected when nothing else references the key.
Node.js, production and automated checks
Server-side code uses the same tools. Start your app with the inspect flag, then connect Chrome DevTools through chrome://inspect for the same memory profiling capabilities in server-side code, as OpenReplay’s guide explains.
You can also catch regressions before release. The same guide shows a Puppeteer script that measures JavaScript heap size before and after an action, forces garbage collection and throws if growth passes a ratio of 1.1. Treat that number as an example, and tune the threshold to your own app.
If growth appears only now and then, read how to debug intermittent bugs for ways to make it reproducible. When overlapping async work seems to create the extra objects, how to debug race conditions explains how to narrow down timing problems.
Finally, verify your fix the same way you found the leak: repeat the action, force garbage collection, compare snapshots and confirm the delta is gone.
Frequently asked questions
How do I force garbage collection before a snapshot?
Show answer
Click the trash can icon in the Memory tab before taking snapshots. You can also call window.gc() in the console if Chrome is started with the –expose-gc flag.
What is the difference between a leak and high memory use?
Show answer
High usage means your app uses a lot of memory but stays stable. A leak shows memory that keeps growing and never plateaus, even when the workload stays constant.
Why does a snapshot show fewer objects than I expect?
Show answer
Taking a snapshot always starts with garbage collection, and snapshots show only objects reachable from the global object. Unreachable garbage is gone by the time you look.
Can I use the same tools on a Node.js server?
Show answer
Yes. Run your app with the inspect flag and connect Chrome DevTools through chrome://inspect to use the same memory profiling capabilities.



