What Is a Memory Leak? Causes in the Browser and How to Find One
A memory leak is allocated memory your code can never reclaim because a reference stays reachable. Here is what causes leaks in the browser, how to find them in the heap profiler, and how a slow-degrading session replay makes one reproducible.

Definition
A memory leak is memory your JavaScript allocated but can never reclaim, because a reference to it stays reachable from the global object long after the data is needed. The garbage collector frees only what it proves unreachable, so a forgotten reference keeps the heap growing until the tab slows or crashes.
Every object you create in JavaScript lives on the heap. The engine reclaims that space automatically through a mark-and-sweep garbage collector that starts at a set of roots (the global object) and frees anything it cannot reach by following references. A leak is not a failure of the collector. It is the collector working exactly as designed on a graph where your data is still reachable through a path you forgot about.
This matters because the symptom is gradual, not immediate. The Chrome DevTools team draws a sharp diagnostic line: a page whose performance gets progressively worse over time indicates a memory leak, while one that is consistently bad indicates memory bloat. The progressive-degradation curve is the leak signature, and it is the single most reliable thing to look for before you open a profiler.

The four classic causes of browser memory leaks
Most front-end leaks reduce to four patterns. Learn to recognize them in code review and you will catch the majority before they ever reach a profiler.
- Detached DOM nodes. You remove an element from the page, but a variable, a cache, or a framework ref still points at it. Because a child node holds a reference to its parent and up the chain, one held child can pin an entire subtree.
- Forgotten event listeners. You call
addEventListeneron mount and never callremoveEventListeneron unmount. The handler closure keeps its captured scope alive for as long as the target exists. - Long-lived closures. A closure that captures a large object and is itself stored somewhere durable (a module-level array, a subscription) keeps that object reachable indefinitely.
- Uncleared timers. A
setIntervalwhose callback references component state, never paired withclearInterval, leaks on every interval forever.
Accidental globals and ever-growing caches round out the list. An array you only ever push to is a leak with a slower fuse.
Why you cannot just "free" the memory
Developers coming from C sometimes look for a free() equivalent. There is none. Per MDN, all modern JavaScript engines ship a mark-and-sweep collector rooted at the global object, and it is not possible to programmatically trigger garbage collection in JavaScript. You cannot force-free a leaked object. The only lever you have is reachability: remove the last reference and the collector will reclaim it on its own schedule.
That is why the fix is always structural. To stop a listener leak you remove the listener; addEventListener accepts an AbortSignal and a { once: true } option so the handler is removed automatically via controller.abort() or after a single invocation. MDN's own memory-issues note warns that anonymous handlers cannot be removed at all because no reference to the anonymous function is kept. Name your handlers, or tie them to an abort signal you control.
How to find a leak in the heap profiler
The workflow in the Chrome DevTools Memory panel is a controlled experiment:
- Snapshot one. Load the page to a stable state and record a heap snapshot.
- Exercise the suspect. Perform the action you think leaks several times: open and close a modal, navigate a route back and forth, mount and unmount a list.
- Snapshot two. Record again and switch to the Comparison view to see exactly which objects were allocated between snapshots and never freed.
To isolate detached nodes specifically, type Detached into the Class filter; the Summary view also offers an Objects retained by detached nodes filter. Then read the Retainers pane at the bottom of the panel, which shows precisely which JavaScript object is keeping the node alive. That retainer chain is your bug. For a live view, enable the Memory checkbox in the Performance panel and watch for a heap line that sawtooths upward without returning to baseline.
Preventing leaks before they ship
Finding a leak is debugging; not creating one is design. Almost every front-end leak is the missing other half of a lifecycle: you registered something on the way in and forgot to unregister it on the way out. The most durable fix is to bind every subscription to a single cancellation token so cleanup cannot be forgotten. An AbortController does exactly that for listeners, timers, fetches, and observers at once.
function mountWidget(node) {
// One controller owns every subscription this widget makes.
const controller = new AbortController();
const { signal } = controller;
// Listeners auto-remove when the signal aborts — no removeEventListener bookkeeping.
window.addEventListener('resize', onResize, { signal });
node.addEventListener('click', onClick, { signal });
// The same signal cancels in-flight fetches and observers.
fetch('/api/data', { signal }).catch(() => {});
const observer = new ResizeObserver(onResize);
observer.observe(node);
signal.addEventListener('abort', () => observer.disconnect(), { once: true });
// Return ONE teardown. Call it on unmount and every reference is released.
return () => controller.abort();
}Catching leaks in production
Heap snapshots are a lab tool. In production you want a cheap, continuous signal. performance.measureUserAgentSpecificMemory() estimates whole-page memory, including the DOM and JavaScript across iframes and workers, which makes it suitable for spotting slow upward drift on real user sessions. The catch: it is Chromium-only since Chrome 89 and requires a secure, cross-origin-isolated context, so you will need the right COOP and COEP headers in place. Sample it on a timer, ship the readings to your telemetry, and alert on a trend rather than a threshold.
How this shows up in a real BugMojo bug report. A leak is a terrible bug to file by hand, because the repro is "use the app for twenty minutes." By the time a tester notices the tab dragging, the interaction that caused it is long gone. BugMojo treats the leak as a reproducible artifact instead of a one-off profiling session. The browser extension records the slow-degrading session as an rrweb session replay timeline, and it captures the console memory warnings inline as they fire (the out-of-memory messages, the "Maximum update depth" loops). Because both the replay growth pattern and the console trail are exposed to an AI agent over BugMojo's MCP server, a tool like Claude Code or Cursor can read them together and pinpoint the leaking interaction (the modal opened forty times, the listener never removed) without an engineer re-reproducing the slow leak by hand. Competitors stop at human-driven heap snapshots; the agent-readable replay-plus-console bundle is what closes the loop.
BugMojo records the degrading session, captures the console memory warnings, and hands the whole bundle to your AI coding agent over MCP — so the leak gets triaged without a 20-minute manual repro.
See how BugMojo captures bugsFrequently asked questions
Frequently asked questions
Sources
- Fix memory problems — Chrome DevTools (memory leak vs. memory bloat; detached DOM; frequent GC) — Google / Chrome for Developers (2024)
- Record heap snapshots — Chrome DevTools (Detached class filter, retainers, comparison view) — Google / Chrome for Developers (2024)
- Memory management — MDN (mark-and-sweep, reachability, circular references, no manual GC) — MDN Web Docs (Mozilla) (2025)
- EventTarget.addEventListener() — MDN (AbortSignal, {once:true}, listener memory issues) — MDN Web Docs (Mozilla) (2025)
- Monitor your web page's total memory usage with measureUserAgentSpecificMemory() — web.dev — web.dev (Google) (2024)
Get bug-tracking insights, weekly.
Engineering deep-dives, QA playbooks, and honest tool comparisons. No spam — unsubscribe in one click.

