Memory Management & Memory Leak Elimination
In long-running Single Page Applications (SPAs), dashboards, and enterprise portals, memory leaks silently degrade browser performance over time. As memory bloats, mobile devices run out of memory (OOM), garbage collection pauses freeze the UI for hundreds of milliseconds, and browser tabs ultimately crash.
In this lesson, we explore how V8 manages memory across the Stack and Heap, how Generational Garbage Collection works, the eight most common front-end memory leak patterns, and how to track down retainers using Chrome DevTools Heap Snapshots.
┌────────────────────────────────────────────────────────────┐
│ V8 Memory Architecture │
├────────────────────────────────────────────────────────────┤
│ 1. Stack: Primitives & Function Execution Frame Pointers │
│ 2. Heap (Dynamically Allocated Objects & Closures): │
│ ├── New Space (Young Generation - Semi-Spaces 1 & 2) │
│ │ └── Collected frequently via fast Scavenge (Minor GC│
│ ├── Old Space (Old Generation): │
│ │ └── Survived objects collected via Mark-Sweep/Compact│
│ └── Large Object Space & Code Space │
└────────────────────────────────────────────────────────────┘
1. V8 Generational Garbage Collection: Scavenger & Major GC
V8's Garbage Collector relies on the Generational Hypothesis: most objects in JavaScript die young (short-lived temporary variables inside function scopes).
- Young Generation (New Space): Small memory zone (1-64 MB) divided into two semi-spaces (From-Space and To-Space). When memory fills, a fast Scavenger (Minor GC) copies surviving live objects to the other semi-space and discards dead ones.
- Old Generation (Old Space): Objects that survive two consecutive Scavenge cycles are promoted to Old Space.
- Major GC (Mark-Sweep-Compact): When Old Space fills, V8 runs a full GC cycle:
- Marking: Traverses the object graph starting from GC Roots (global window, active call stack, DOM tree).
- Sweeping: Reclaims memory addresses of unreachable objects.
- Compacting: Relocates fragmented memory pages to prevent fragmentation.
2. The 8 Most Common Front-End Memory Leaks
1. Forgotten Event Listeners on Global Objects
// ❌ Memory Leak: Component destroys, but window retains callback & closure scope
export function mountDashboardWidget() {
const largeData = new Array(1000000).fill("data");
window.addEventListener("resize", () => {
console.log(largeData.length);
});
}
// ✅ Fix: Always remove event listeners or pass an AbortSignal
export function mountDashboardWidgetFixed(abortSignal) {
const largeData = new Array(1000000).fill("data");
window.addEventListener("resize", () => {
console.log(largeData.length);
}, { signal: abortSignal });
}
2. Detached DOM Node Leaks
A detached DOM node occurs when an element is removed from the active DOM tree (parent.removeChild(el)), but a JavaScript variable still retains a reference to it. Because the node is referenced, the browser cannot garbage collect the element or any of its child nodes:
// ❌ Detached DOM Leak
let cachedButton = document.getElementById("submit-btn");
document.body.removeChild(cachedButton); // Removed from DOM, but retained by 'cachedButton'!
// ✅ Fix
cachedButton = null; // Clear reference to allow GC
3. Uncleaned setInterval & setTimeout Timers
// ❌ Timer Leak: Interval continues running forever in background
const timer = setInterval(() => {
fetchLiveData();
}, 1000);
// ✅ Fix: Clear timer on component unmount
clearInterval(timer);
4. RxJS / WebSocket / Event Emitter Subscriptions
Forgetting to unsubscribe from active WebSocket streams or RxJS Observables retains the entire subscriber callback and surrounding closure state.
5. Accidental Global Variables (window.cache = {})
6. Unbounded In-Memory Caches
Caches that grow indefinitely without an eviction policy (e.g., LRU cache or WeakMap).
3. Chrome DevTools Heap Snapshot Diagnostics
To isolate memory leaks:
- Open Chrome DevTools → Memory panel.
- Select Heap snapshot → Click Take snapshot (Baseline Snapshot 1).
- Perform the user action (e.g., open a modal, navigate to a route, and close/navigate back).
- Click the trash can icon (🧹 Collect garbage) to force a GC cycle.
- Take Snapshot 2.
- Change the perspective filter from Summary to Comparison between Snapshot 2 and Snapshot 1.
- Sort by # Delta or Alloc. Size to see which constructor objects failed to deallocate.
- Filter for
Detachedto locate all detached HTML elements retaining memory!
Constructor │ Distance │ Shallow Size │ Retained Size
▼ Detached HTMLDivElement │ 4 │ 72 B │ 4.2 MB (Retained!)
▼ onClick handler │ 3 │ 48 B │ 4.2 MB
▼ context in AppRouter │ 2 │ 128 B │ 4.2 MB
▼ Window (GC Root) │ 1 │ 80 B │ 4.2 MB
Inspecting the Retainers Tree at the bottom reveals the exact chain of references keeping the detached node alive all the way back to the GC Root.
Summary
- JavaScript memory divides into Call Stack (execution frames) and Heap (dynamic objects).
- V8 uses generational garbage collection: fast Scavenger for Young Space and Mark-Sweep-Compact for Old Space.
- Memory leaks occur when unreachable components remain referenced by GC Roots (event listeners, timers, closures).
- Detached DOM nodes occur when JavaScript variables hold references to removed DOM elements.
- Chrome DevTools Heap Snapshots and Retainer trees identify the exact variable preventing garbage collection.
Best Practices
- Pass
AbortSignalto Event Listeners: Automatically unbind event listeners on component unmount with{ signal: controller.signal }. - Clear All Timers & Intervals on Teardown: Store
timerIdand always callclearTimeout/clearIntervalin cleanup hooks. - Use WeakMap for Object-Associated Caches: Ensure cached metadata evaporates automatically when the host object is garbage collected.
- Profile Heap Delta in CI / Staging: Compare memory before and after repeated user workflows to catch memory leaks before production releases.