JavaScript manages memory automatically, but “automatic” does not mean “leak-proof.” Forgotten event listeners, closures holding large arrays, detached DOM nodes, and Map entries that accumulate silently — these are real production problems that cause SPAs to consume gigabytes over hours of use. This guide explains how V8’s garbage collector works and how to find and fix memory leaks before they reach production.
⚡ TL;DR: V8 GC uses generational collection: most objects die young (Minor GC, fast). Long-lived objects promoted to Old Generation (Major GC, slower). Memory leaks = objects the GC cannot collect because something still holds a reference. Find them with Chrome DevTools heap snapshots. Prevent them with WeakMap, WeakRef, and proper cleanup.
How V8 garbage collection works
// V8 memory layout:
// New Space (Young Generation): ~1-8MB, most objects start here
// - Scavenger GC (Minor GC): runs frequently (~ms)
// - Objects that survive 2 scavenges are promoted to Old Space
// Old Space (Old Generation): larger, long-lived objects
// - Mark-Sweep-Compact (Major GC): less frequent, can take 100ms+
// - Stop-the-world: JS execution pauses during full GC
// Key insight:
// Most objects die young (short-lived temp variables, small objects)
// New Space is small → Minor GC is fast
// Only long-lived objects reach Old Space → Major GC less frequent
// An object is collectable when:
// No more references pointing to it from roots (global, call stack, closures)
// Reachability: GC traces from roots, unreachable = collectable
// An object LEAKS when:
// Something still references it that you forgot about
// Most common: event listener, closure, Map/Set entry, timer callbackThe 4 most common memory leak patterns
// Leak 1: Forgotten event listeners
class SearchComponent {
init() {
document.addEventListener('keydown', this.handleKey); // Added
// Component destroyed, but listener never removed
// document still holds reference to handleKey which holds 'this'
}
destroy() {
document.removeEventListener('keydown', this.handleKey); // Must remove!
}
}
// Leak 2: Closure over large data
function processData() {
const bigArray = new Array(1_000_000).fill(0); // 8MB
return function getBigArrayLength() {
return bigArray.length; // bigArray kept alive by closure!
};
}
// Fix: only capture what you need
function processData() {
const length = new Array(1_000_000).fill(0).length; // Compute, then release
return () => length; // Only captures the number, not the array
}
// Leak 3: Growing Map/Set without cleanup
const cache = new Map();
setInterval(() => {
const key = generateKey();
cache.set(key, expensiveCompute()); // Grows forever!
}, 100);
// Fix: bounded cache (LRU) or use WeakMap (GC-friendly)
// Leak 4: Detached DOM nodes
const detached = document.getElementById('modal');
document.body.removeChild(detached);
const store = { modal: detached }; // Still referenced! GC cannot collect.Finding leaks with Chrome DevTools
// Step 1: Open DevTools → Memory tab
// Step 2: Take baseline heap snapshot (before the action)
// Step 3: Perform the action suspected of leaking
// Step 4: Force GC (trash can icon)
// Step 5: Take second heap snapshot
// Step 6: Compare snapshots — select "Comparison" view
// What to look for:
// Objects with positive "Delta" (more exist after action)
// Large "Shallow size" increases (direct memory of object)
// Large "Retained size" increases (object + everything it keeps alive)
// Detecting leaks with Performance Monitor:
// DevTools → ... menu → Performance Monitor
// Watch: JS heap size over time
// Normal: heap grows and drops (GC runs)
// Leak: heap grows monotonically without dropping
// Node.js: use --inspect flag + Chrome DevTools remotely
// node --inspect server.js
// Open chrome://inspect in ChromeWeakMap and WeakRef for leak-proof code
// WeakMap: keys are weakly referenced — GC can collect the key object
// When key is collected, the WeakMap entry disappears automatically
// Attach metadata to DOM nodes without preventing GC
const domMetadata = new WeakMap();
const element = document.getElementById('chart');
domMetadata.set(element, { initialized: true, data: [...] });
// When element is removed from DOM and no other reference:
// GC collects element, domMetadata entry automatically removed
// Map: element would be kept alive by Map key reference forever
// WeakRef: hold reference that does NOT prevent GC
let expensiveObject = { data: new Array(1_000_000) };
const ref = new WeakRef(expensiveObject);
expensiveObject = null; // Remove strong reference
// Later:
const obj = ref.deref(); // May be undefined if GC collected it
if (obj) use(obj); // Always check!Memory management checklist
- ✅ Remove event listeners on component/page cleanup
- ✅ Clear timers:
clearTimeout,clearIntervalon unmount - ✅ Use WeakMap instead of Map for DOM-keyed metadata
- ✅ Limit cache size — LRU eviction or TTL expiry
- ✅ Monitor JS heap in Chrome Performance Monitor
- ✅ Heap snapshot comparison to find specific leaking objects
- ❌ Never store DOM references in long-lived data structures without WeakRef
- ❌ Never use closures that capture more data than needed
JavaScript memory management connects to the WeakMap and WeakRef deep dive — this post covers the problem, that post covers the solution in depth. The V8 JIT and memory interaction is covered in the V8 JIT optimization guide. External reference: Firefox Memory tool documentation.
Free Weekly Newsletter
🚀 Don’t Miss the Next Cheat Code
Related: Node.js Streams Backpressure: The Memory Leak That Kills Servers at 3AM · Node Memory Leaks: Detached Cache Graphs That Heap Snapshots Reveal · TypeScript for JavaScript Developers: The Migration Guide That Actually Works
Related: JavaScript Closures, Scope, and the Module Pattern: The Complete Guide · JavaScript Promises: The Complete Guide From Basics to Advanced Patterns · JavaScript Async/Await: Every Pattern, Pitfall, and Performance Trick
Watch: 100 Days of AI on YouTube
Short videos from CheatCoders, one AI engineering topic per day.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Claude Code vs Cursor vs GitHub Copilot: Choose by Workflow, and Set Up One Repo That Works With All Three
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
- Python String Methods: Every str Method With Real Production Examples
- REST API Design Best Practices: The Patterns That Make APIs a Joy to Use
Newly added
- AWS Systems Manager Parameter Store: Hierarchical Runtime Config for Coding Agents
- Amazon Cognito Identity Pools: Federate Coding-Agent Tool Callers Without Long-Lived Keys
- Amazon Athena for Coding Agents: Query Tool Audit Logs and Eval Traces on S3 Without a Warehouse
- AWS Batch for Coding Agents: Parallel Overnight PR Reviews and Long-Running Jobs Without Lambda Timeouts
- Amazon Bedrock Prompt Management: Version and A/B Test Coding-Agent System Prompts
Deep-dive PDF
Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.
Already subscribed? or open the subscribe page.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
