Memory leaks occur when JavaScript retains references to objects that are no longer needed, causing steadily increasing memory consumption that degrades performance and can eventually crash the browser tab.
Listeners not removed on cleanup keep their callbacks and closures alive. Always remove listeners when components unmount or elements are removed.
setInterval runs indefinitely until cleared. Uncleared timers in unmounted components retain references to stale data and closures.
Removing a DOM element while keeping a JavaScript reference to it prevents the entire subtree from being garbage collected.
Heap Snapshots compare allocations between actions, Allocation Timeline tracks persistent allocations, and Performance Monitor shows real-time heap growth.
Weak references allow associated objects to be garbage collected, making them ideal for caches and metadata stores that should not prevent cleanup.
JavaScript uses automatic garbage collection — the engine periodically identifies objects that are no longer reachable from the root (global scope, call stack) and frees their memory. A memory leak occurs when objects that should be garbage collected remain reachable due to unintended references. Over time, this causes the heap to grow, the garbage collector to run more frequently, and eventually the page to slow down or crash.
Forgotten event listeners: Adding event listeners to DOM elements or global objects (window, document) without removing them is the most common leak. When a component unmounts or an element is removed, its listeners keep the callback — and everything in its closure — alive. Always call removeEventListener in cleanup code, or use { once: true } for one-time listeners. In React, return a cleanup function from useEffect.
Uncleared timers and intervals: setInterval callbacks run indefinitely until cleared with clearInterval. If you set up an interval in a component that unmounts without clearing it, the callback continues executing and retaining references to stale data. The same applies to setTimeout if the component unmounts before it fires.
Detached DOM nodes: When you remove a DOM element from the document but retain a JavaScript reference to it (in a variable, array, or Map), the entire DOM subtree remains in memory. This is especially common with caching patterns that store removed elements for potential reuse.
Closures capturing large scopes: A closure retains its entire lexical environment. If a small callback closes over a scope containing a large array or object, that entire scope stays in memory as long as the callback exists. This is subtle because the leak is not in the closure itself but in what the closure inadvertently captures.
Global variables: Accidentally creating global variables (forgetting let/const, or assigning to this in non-strict mode) attaches data to the window object, where it is never garbage collected.
Chrome DevTools provides three key tools for memory analysis:
Heap Snapshots: Take a snapshot, perform an action, take another snapshot, and compare. Objects that appear in the second but not the first are potential leaks. Filter by "Objects allocated between snapshots" to find them.
Allocation Timeline: Records memory allocations over time. Blue bars that remain (never get garbage collected) indicate leaks. This helps identify which operations cause persistent allocations.
Performance Monitor: The real-time panel shows JS heap size, DOM nodes, and event listeners count. A steadily increasing heap or DOM node count during normal usage strongly indicates a leak.
WeakMap and WeakSet hold "weak" references to their keys — if the key object has no other references, it can be garbage collected even though it is in the WeakMap. This makes them ideal for associating metadata with objects without preventing their cleanup. WeakRef (ES2021) provides a weak reference to any object, and FinalizationRegistry lets you register a callback that fires when an object is garbage collected.
Always clean up in useEffect return functions. Use AbortController to cancel fetch requests on unmount. Prefer event delegation over individual listeners. Use WeakMap for object-keyed caches. Avoid storing DOM references in long-lived data structures. In production, use automated memory monitoring to catch leaks before users do.
Memory leaks in JavaScript are not about unreleased memory in the traditional sense — the garbage collector handles that. They are about unintended references that prevent garbage collection. The fix is always to remove the reference: clear the timer, remove the listener, nullify the variable, or use weak references.
Fun Fact
JavaScript's garbage collector uses a 'mark-and-sweep' algorithm. It starts from root objects (global scope, call stack), marks everything reachable, then sweeps (frees) everything unmarked. V8's garbage collector is called Orinoco and can perform most of its work concurrently on a background thread, pausing the main thread for less than 1ms in most cases.