Interview Importance: π΄ Critical β Asked in 70% of senior frontend interviews. Essential for understanding memory management, performance optimization, and preventing memory leaks in production applications.
Garbage Collection (GC) is an automatic memory management mechanism that identifies and reclaims memory occupied by objects that are no longer needed by the program. It frees developers from manual memory allocation/deallocation, preventing common bugs like dangling pointers and memory leaks.
+--------------------------------------------------------------+
| JAVASCRIPT HEAP MEMORY |
| |
| +---------+ +---------+ +---------+ |
| | Object A|β---+ Object B| | Object C| (unreachable) |
| | (root) | |(reachable) +---------+ |
| +----+----+ +---------+ ^ |
| | | |
| | +---------+ | |
| +--------βΊ| Object D| | |
| |(reachable) | |
| +---------+ | |
| | |
| GC MARKS C AS GARBAGE ---------------+
| AND RECLAIMS ITS MEMORY |
+--------------------------------------------------------------+
Think of GC like an automated office cleaning service:
+--------------------------------------------------------------+
| π’ Your JavaScript Program = Office Building |
| |
| Active Workers (Variables) = Desks with People |
| +- CEO Desk (Global Scope) |
| +- Manager Desks (Function Scopes) |
| +- Employee Desks (Local Variables) |
| |
| Files on Desks (Objects) = Memory Being Used |
| |
| π§Ή Cleaning Service (GC) comes at night and: |
| 1. Checks which desks are occupied (reachable) |
| 2. Identifies empty desks with leftover files |
| 3. Throws away files on empty desks (reclaims memory) |
| 4. Keeps files on occupied desks (preserves data) |
+--------------------------------------------------------------+
// Memory is allocated when objects are created let user = { name: "Alice", age: 30 }; // Object created in memory // Memory is still in use (reachable from 'user' variable) console.log(user.name); // "Alice" // Object becomes eligible for garbage collection user = null; // No more references to the object // GC will eventually reclaim the memory (automatic) // Developer doesn't need to do anything!
| Concern | Without GC (Manual Memory) | With GC (JavaScript) | Impact |
|---|---|---|---|
| Memory leaks | Developer forgets to free memory | Automatic cleanup of unreachable objects | 99% fewer memory bugs |
| Development speed | Must track every allocation | Focus on business logic | 5x faster development |
| Dangling pointers | Accessing freed memory crashes | Impossible - memory stays until unreachable | Zero pointer errors |
| Memory fragmentation | Manual defragmentation needed | GC compacts memory automatically | Better performance |
| Cognitive load | Track object lifecycles manually | Declare and forget | Mental freedom |
Performance Benefits:
The Mark-and-Sweep algorithm is the foundation of garbage collection in V8 (Chrome/Node.js) and SpiderMonkey (Firefox).
// Simplified GC mark-and-sweep pseudo-code class GarbageCollector { constructor() { this.heap = new Set(); // All objects in memory this.roots = new Set(); // Global variables, call stack } // Phase 1: Mark all reachable objects markPhase() { const marked = new Set(); const toVisit = [...this.roots]; while (toVisit.length > 0) { const obj = toVisit.pop(); if (marked.has(obj)) continue; // Already visited marked.add(obj); // Mark as reachable // Add all referenced objects to visit queue const references = this.getReferences(obj); toVisit.push(...references); } return marked; } // Phase 2: Sweep (delete) unmarked objects sweepPhase(marked) { const garbage = []; for (const obj of this.heap) { if (!marked.has(obj)) { garbage.push(obj); // Unmarked = garbage } } // Reclaim memory garbage.forEach(obj => { this.heap.delete(obj); this.freeMemory(obj); }); return garbage.length; // Objects collected } // Run full GC cycle collect() { const marked = this.markPhase(); // Mark reachable const freed = this.sweepPhase(marked); // Sweep unreachable return freed; } getReferences(obj) { // Return all objects referenced by this object return Object.values(obj).filter(v => typeof v === 'object' && v !== null); } freeMemory(obj) { // Actual memory deallocation (engine-specific) console.log(`Freeing memory for object: ${obj.id}`); } }
// Setup: Object graph const global = { user: { name: "Alice", profile: { age: 30 } }, temp: { data: "temp" } }; // Later: temp reference removed global.temp = null; // GC Cycle Begins
Step-by-step execution:
Initial State:
---------------------------------------------------------
Heap: [user_obj, profile_obj, temp_obj]
Roots: [global]
Object Graph:
global --+--> user_obj --> profile_obj
+--> null (was temp_obj)
temp_obj: UNREACHABLE (no references)
PHASE 1: MARK (Starting from roots)
---------------------------------------------------------
Step 1: Visit global (root)
marked = [global]
toVisit = [user_obj]
Step 2: Visit user_obj
marked = [global, user_obj]
toVisit = [profile_obj]
Step 3: Visit profile_obj
marked = [global, user_obj, profile_obj]
toVisit = []
Mark phase complete: 3 objects marked as reachable
PHASE 2: SWEEP (Clean unmarked objects)
---------------------------------------------------------
Step 1: Check heap objects
user_obj: MARKED β -> Keep
profile_obj: MARKED β -> Keep
temp_obj: UNMARKED β -> GARBAGE!
Step 2: Reclaim memory
Free temp_obj -> Memory returned to heap
Final State:
---------------------------------------------------------
Heap: [user_obj, profile_obj]
Freed: 1 object (temp_obj)
Memory reclaimed: ~100 bytes
Reference counting tracks how many references point to each object. When count reaches zero, memory is freed immediately.
class ReferenceCounting { constructor() { this.refCounts = new WeakMap(); // Object -> count } addReference(obj) { const count = this.refCounts.get(obj) || 0; this.refCounts.set(obj, count + 1); } removeReference(obj) { const count = this.refCounts.get(obj) || 0; if (count <= 1) { this.freeObject(obj); // Count reached 0 this.refCounts.delete(obj); } else { this.refCounts.set(obj, count - 1); } } freeObject(obj) { console.log('Freeing:', obj); } } // Example usage const rc = new ReferenceCounting(); let obj = { data: "test" }; // refCount = 1 let copy = obj; // refCount = 2 copy = null; // refCount = 1 obj = null; // refCount = 0 -> FREED immediately
Problem with Reference Counting: Circular References
// This leaks memory in pure reference counting! function createCircularReference() { const obj1 = {}; const obj2 = {}; obj1.ref = obj2; // obj2 refCount = 1 obj2.ref = obj1; // obj1 refCount = 1 // Both objects reference each other // Even when function returns, refCount never reaches 0! return null; } createCircularReference(); // Memory leak in reference counting
Why Modern Engines Use Mark-and-Sweep:
An object is reachable if it can be accessed from a root reference through a chain of references.
Roots (always reachable):
window in browsers, global in Node.js)// What makes objects reachable? // 1. Global variables (root) window.userData = { name: "Alice" }; // REACHABLE (root) // 2. Function scope function processData() { const local = { temp: "data" }; // REACHABLE (on call stack) return local; } // 3. Closures function createCounter() { const state = { count: 0 }; // REACHABLE (closure) return () => state.count++; } // 4. Event listeners button.addEventListener('click', function handler() { const data = { clicks: 0 }; // REACHABLE (handler referenced) });
Modern engines optimize GC using the generational hypothesis: most objects die young.
+--------------------------------------------------------------+
| GENERATIONAL GC STRATEGY |
| |
| Young Generation (Nursery) Old Generation (Tenured) |
| +---------------------+ +----------------------+ |
| | New objects | | Long-lived objects | |
| | Short-lived |----βΆ | Survived multiple GCs| |
| | GC runs frequently | | GC runs rarely | |
| | ~90% of allocations | | ~10% promoted here | |
| +---------------------+ +----------------------+ |
| Minor GC (fast, frequent) Major GC (slow, rare) |
| ~10ms every few seconds ~100ms every few minutes |
+--------------------------------------------------------------+
Why This Matters:
// Young generation (dies quickly) function render() { const tempData = processInput(); // Allocated in young gen updateUI(tempData); // Used briefly // tempData becomes unreachable -> Minor GC collects quickly } // Old generation (long-lived) const appState = { user: {}, config: {} }; // Survives many GCs // Promoted to old generation -> Major GC handles eventually
WeakMap and WeakSet allow references that DON'T prevent garbage collection.
// Regular Map prevents GC const cache = new Map(); let user = { name: "Alice" }; cache.set(user, "cached data"); // user is REACHABLE through cache user = null; // user object STILL IN MEMORY (cache holds it) // WeakMap allows GC const weakCache = new WeakMap(); let user2 = { name: "Bob" }; weakCache.set(user2, "cached data"); // Weak reference user2 = null; // user2 CAN BE COLLECTED (weak reference) console.log(weakCache.has(user2)); // false (after GC)
Use Cases for Weak References:
// Practical: Private data with WeakMap const privateData = new WeakMap(); class User { constructor(name, ssn) { this.name = name; // Public privateData.set(this, { ssn }); // Private (won't leak) } getSSN() { return privateData.get(this).ssn; } } let user = new User("Alice", "123-45-6789"); console.log(user.getSSN()); // "123-45-6789" user = null; // Both user AND private data are GC'd together
Memory Leak: When memory that is no longer needed remains reachable, preventing garbage collection.
// β BAD: Timer creates persistent reference function startUpdates() { const data = new Array(1000000).fill('data'); // Large object setInterval(() => { console.log(data.length); // Closure references data }, 1000); // Even if startUpdates() is done, data stays in memory forever! } startUpdates(); // β GOOD: Clear timer to allow GC function startUpdatesFixed() { const data = new Array(1000000).fill('data'); const timerId = setInterval(() => { console.log(data.length); }, 1000); // Return cleanup function return () => clearInterval(timerId); // Allows GC when no longer needed } const cleanup = startUpdatesFixed(); // Later: cleanup(); -> data can be garbage collected
// β BAD: Event listener holds references class DataViewer { constructor(element) { this.element = element; this.data = new Array(1000000).fill('data'); // Large data // Anonymous function creates closure over 'this' this.element.addEventListener('click', () => { console.log(this.data.length); }); } } const viewer = new DataViewer(document.getElementById('btn')); // Even if we remove the DOM element, viewer.data stays in memory! // β GOOD: Remove listeners to allow GC class DataViewerFixed { constructor(element) { this.element = element; this.data = new Array(1000000).fill('data'); // Named method for easier cleanup this.handleClick = () => { console.log(this.data.length); }; this.element.addEventListener('click', this.handleClick); } destroy() { this.element.removeEventListener('click', this.handleClick); this.element = null; this.data = null; // Allow GC } } const viewer2 = new DataViewerFixed(document.getElementById('btn')); // Later: viewer2.destroy(); -> Everything can be GC'd
// β BAD: Keeping references to removed DOM nodes const cache = {}; function addToCache() { const element = document.getElementById('myDiv'); cache.myDiv = element; // Store reference // Later: Remove from DOM element.parentNode.removeChild(element); // DOM node is detached but STILL IN MEMORY via cache! } // β GOOD: Clear references when done function addToCacheFixed() { const element = document.getElementById('myDiv'); // Use WeakMap for automatic cleanup const weakCache = new WeakMap(); weakCache.set(element, { data: 'cached' }); element.parentNode.removeChild(element); // No strong reference -> GC can collect it }
// β BAD: Closure captures entire scope function processData() { const largeData = new Array(1000000).fill('data'); // 8MB+ const metadata = { count: largeData.length }; // This closure captures ENTIRE scope (including largeData)! return function getCount() { return metadata.count; // Only uses metadata }; } const getCount = processData(); // largeData is STUCK in memory even though we only need metadata! // β GOOD: Minimize closure scope function processDataFixed() { const largeData = new Array(1000000).fill('data'); const count = largeData.length; // Copy primitive // Clear large data before creating closure // (in real code, this happens naturally as function exits) return function getCount() { return count; // Only captures small primitive }; // largeData goes out of scope -> Can be GC'd }
// β BAD: Accidental globals function createData() { // Missing 'const'/'let' creates global! userData = { name: "Alice", data: new Array(1000000) }; // userData is now window.userData -> NEVER garbage collected! } createData(); // β GOOD: Always use const/let/var function createDataFixed() { const userData = { name: "Alice", data: new Array(1000000) }; return userData; // Explicit return // Goes out of scope when no longer referenced } const data = createDataFixed(); // Later: data = null; -> Can be GC'd
// Modern engines handle this correctly, but good to know function createCircular() { const obj1 = {}; const obj2 = {}; obj1.ref = obj2; obj2.ref = obj1; // Circular reference // In old IE (pre-IE9), this would leak // Modern mark-and-sweep handles it fine } // β POTENTIAL ISSUE: DOM + JS circular references (old browsers) const element = document.getElementById('myDiv'); const data = { element: element }; element.userData = data; // Circular: DOM <-> JS // β GOOD: Break circles explicitly (defensive) function cleanup() { element.userData = null; data.element = null; }
// β BAD: Common React memory leak function UserProfile() { const [user, setUser] = useState(null); useEffect(() => { // Start fetching fetch('/api/user') .then(res => res.json()) .then(data => { setUser(data); // β οΈ Component might be unmounted! }); // Missing cleanup! }, []); return <div>{user?.name}</div>; } // β GOOD: Cleanup with AbortController function UserProfileFixed() { const [user, setUser] = useState(null); useEffect(() => { const controller = new AbortController(); fetch('/api/user', { signal: controller.signal }) .then(res => res.json()) .then(data => { setUser(data); // Safe: won't run if aborted }) .catch(err => { if (err.name === 'AbortError') { console.log('Fetch aborted'); } }); // Cleanup function return () => { controller.abort(); // Cancel fetch on unmount }; }, []); return <div>{user?.name}</div>; }
// β BAD: Accumulating DOM nodes class InfiniteScroll { constructor() { this.items = []; // Keeps growing forever! this.container = document.getElementById('list'); } addItems(newItems) { newItems.forEach(item => { const element = document.createElement('div'); element.textContent = item; this.container.appendChild(element); this.items.push(element); // Memory keeps growing }); } } // β GOOD: Virtual scrolling (render only visible items) class VirtualScroll { constructor() { this.allData = []; // Just data, not DOM this.container = document.getElementById('list'); this.visibleItems = new Map(); // Track rendered items this.setupScroll(); } setupScroll() { this.container.addEventListener('scroll', () => { this.render(); }); } render() { const { scrollTop, clientHeight } = this.container; const startIdx = Math.floor(scrollTop / 50); // Item height = 50 const endIdx = startIdx + Math.ceil(clientHeight / 50); // Remove items outside viewport for (const [idx, element] of this.visibleItems) { if (idx < startIdx || idx > endIdx) { element.remove(); // DOM removed -> GC can collect this.visibleItems.delete(idx); } } // Add items in viewport for (let i = startIdx; i <= endIdx; i++) { if (!this.visibleItems.has(i) && this.allData[i]) { const element = document.createElement('div'); element.textContent = this.allData[i]; this.container.appendChild(element); this.visibleItems.set(i, element); } } } destroy() { this.visibleItems.clear(); // Clear references this.container.innerHTML = ''; // Remove all DOM } }
// β GOOD: LRU Cache with automatic eviction class LRUCache { constructor(maxSize = 100) { this.maxSize = maxSize; this.cache = new Map(); // Preserves insertion order } get(key) { if (!this.cache.has(key)) return null; // Move to end (most recently used) const value = this.cache.get(key); this.cache.delete(key); this.cache.set(key, value); return value; } set(key, value) { // Remove if exists (to update position) if (this.cache.has(key)) { this.cache.delete(key); } // Add to end this.cache.set(key, value); // Evict oldest if over limit if (this.cache.size > this.maxSize) { const firstKey = this.cache.keys().next().value; this.cache.delete(firstKey); // Oldest item -> GC can collect } } clear() { this.cache.clear(); // All items eligible for GC } } // Usage const cache = new LRUCache(1000); cache.set('user:1', { name: "Alice" }); cache.get('user:1'); // Move to end // When limit exceeded, oldest items are automatically removed
// β GOOD: Proper Web Worker cleanup class WorkerPool { constructor(workerScript, poolSize = 4) { this.workers = []; this.tasks = []; for (let i = 0; i < poolSize; i++) { const worker = new Worker(workerScript); this.workers.push({ worker, busy: false }); } } execute(data) { return new Promise((resolve, reject) => { const task = { data, resolve, reject }; this.tasks.push(task); this.processQueue(); }); } processQueue() { if (this.tasks.length === 0) return; const availableWorker = this.workers.find(w => !w.busy); if (!availableWorker) return; const task = this.tasks.shift(); availableWorker.busy = true; const { worker } = availableWorker; const handleMessage = (e) => { task.resolve(e.data); cleanup(); }; const handleError = (e) => { task.reject(e); cleanup(); }; const cleanup = () => { worker.removeEventListener('message', handleMessage); worker.removeEventListener('error', handleError); availableWorker.busy = false; this.processQueue(); // Process next task }; worker.addEventListener('message', handleMessage); worker.addEventListener('error', handleError); worker.postMessage(task.data); } destroy() { // Terminate all workers -> Allow GC this.workers.forEach(({ worker }) => worker.terminate()); this.workers = []; this.tasks = []; } } // Usage const pool = new WorkerPool('processor.js', 4); await pool.execute({ data: 'process this' }); // Later: pool.destroy(); -> All workers and references cleaned up
Chrome DevTools Memory Profiler:
1. Open DevTools -> Memory Tab
2. Take Heap Snapshot
3. Perform actions (navigate, interact)
4. Take another Heap Snapshot
5. Compare snapshots -> Look for:
- Growing arrays/objects
- Detached DOM nodes
- Event listeners not removed
Interpreting Results:
Snapshot Comparison:
---------------------------------------------------------
Constructor | Delta | Size Delta | # New | # Deleted
--------------------|-------|------------|-------|----------
Array | +250 | +2.5 MB | 300 | 50 <-- LEAK!
HTMLDivElement | +100 | +500 KB | 150 | 50 <-- Detached?
Closure | +50 | +100 KB | 75 | 25 <-- Check!
Object | +10 | +50 KB | 20 | 10 <-- Normal
// Monitor memory usage class MemoryMonitor { constructor() { this.samples = []; this.maxSamples = 100; } sample() { if (!performance.memory) { console.warn('performance.memory not available'); return null; } const sample = { timestamp: Date.now(), usedJSHeapSize: performance.memory.usedJSHeapSize, totalJSHeapSize: performance.memory.totalJSHeapSize, jsHeapSizeLimit: performance.memory.jsHeapSizeLimit }; this.samples.push(sample); if (this.samples.length > this.maxSamples) { this.samples.shift(); // Keep recent samples } return sample; } detectLeak() { if (this.samples.length < 10) return null; // Check if memory consistently increases const recent = this.samples.slice(-10); const increases = recent.slice(1).filter((sample, i) => { return sample.usedJSHeapSize > recent[i].usedJSHeapSize; }); const leakProbability = increases.length / recent.length; return { isLikelyLeaking: leakProbability > 0.7, probability: leakProbability, currentUsage: recent[recent.length - 1].usedJSHeapSize, trend: 'increasing' }; } getReport() { if (this.samples.length === 0) return null; const latest = this.samples[this.samples.length - 1]; const usagePercent = (latest.usedJSHeapSize / latest.jsHeapSizeLimit) * 100; return { usedMB: (latest.usedJSHeapSize / 1024 / 1024).toFixed(2), totalMB: (latest.totalJSHeapSize / 1024 / 1024).toFixed(2), limitMB: (latest.jsHeapSizeLimit / 1024 / 1024).toFixed(2), usagePercent: usagePercent.toFixed(2) + '%', leak: this.detectLeak() }; } } // Usage const monitor = new MemoryMonitor(); setInterval(() => { monitor.sample(); const report = monitor.getReport(); if (report.leak?.isLikelyLeaking) { console.warn('β οΈ Possible memory leak detected!', report); } }, 5000);
Answer:
Modern engines use mark-and-sweep because it handles cycles: obj1.ref = obj2; obj2.ref = obj1;
Answer: Yes, despite automatic GC! Common causes:
// 1. Forgotten event listeners element.addEventListener('click', handler); // Keeps handler + closure in memory // Fix: removeEventListener() on cleanup // 2. Forgotten timers setInterval(() => console.log(data), 1000); // 'data' never released // Fix: clearInterval() // 3. Global variables window.cache = {}; // Never collected // Fix: Limit scope, use WeakMap // 4. Detached DOM nodes const cache = { node: document.getElementById('div') }; node.remove(); // DOM removed but cache.node keeps it // Fix: Clear references
Answer:
Weak references don't prevent garbage collection. WeakMap keys are weakly held.
Use cases:
// Private data const privateData = new WeakMap(); class User { constructor(name) { this.name = name; privateData.set(this, { ssn: '123-45-6789' }); } } // When User instance is GC'd, private data goes too // Caching without leaks const cache = new WeakMap(); cache.set(domNode, computedValue); // If domNode is removed and GC'd, cache entry disappears automatically
Answer: Based on observation that most objects die young:
Benefits:
Example: Temporary objects in render loops die young, app state survives to old gen.
Answer:
Chrome DevTools Memory Profiler:
Performance.memory API:
console.log(performance.memory.usedJSHeapSize);
Memory leak patterns to look for:
Answer: Closures capture their entire lexical scope, keeping it in memory:
// β Captures large data unnecessarily function outer() { const hugeArray = new Array(1000000); const small = 42; return function inner() { return small; // Only needs 'small' but captures entire scope! }; } // β Minimize closure scope function outer() { const hugeArray = new Array(1000000); const small = hugeArray.length; // hugeArray goes out of scope here return function inner() { return small; // Only captures 'small' }; }
Impact: Modern engines optimize this, but be aware of what closures capture in long-lived callbacks.
Answer:
FATAL ERROR: Ineffective mark-compacts near heap limit
Allocation failed - JavaScript heap out of memory
Prevention:
// β BAD: Expecting immediate cleanup function test() { let huge = new Array(1000000); huge = null; // Eligible for GC, but NOT immediate console.log('Memory freed!'); // β Not yet! } // β GOOD: Understand GC is asynchronous function test() { let huge = new Array(1000000); huge = null; // Eligible for GC // GC runs when engine decides (based on heuristics) // You cannot force it (in production) console.log('Memory eligible for GC'); } // For testing only (not in production): if (global.gc) { global.gc(); // Manual GC (requires --expose-gc flag) }
delete Helps GC// β BAD: delete doesn't trigger GC const obj = { a: 1, b: 2, c: 3 }; delete obj.a; // Removes property, but doesn't help GC // obj still exists, just missing 'a' property // β GOOD: Remove references for GC let obj = { a: 1, b: 2, c: 3 }; obj = null; // Now eligible for GC // delete is for removing properties, not memory management
// β BAD: Global cache grows forever window.userCache = {}; function cacheUser(id, data) { window.userCache[id] = data; // Never released! } // After 10,000 users cached = memory leak // β GOOD: Limited cache with eviction class UserCache { constructor(maxSize = 1000) { this.cache = new Map(); this.maxSize = maxSize; } set(id, data) { if (this.cache.size >= this.maxSize) { const firstKey = this.cache.keys().next().value; this.cache.delete(firstKey); // Evict oldest } this.cache.set(id, data); } get(id) { return this.cache.get(id); } } const userCache = new UserCache(1000); // Limited size
// β BAD: React component leaks listeners function ChatWidget() { useEffect(() => { const handler = (msg) => console.log(msg); socket.on('message', handler); // Missing cleanup! }, []); } // Every time component mounts, adds listener // Old listeners never removed -> memory leak // β GOOD: Always cleanup in useEffect function ChatWidget() { useEffect(() => { const handler = (msg) => console.log(msg); socket.on('message', handler); return () => { socket.off('message', handler); // Cleanup! }; }, []); }
| Algorithm | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Mark-and-Sweep | O(n) where n = reachable objects | O(n) for marking set | Modern engines use this |
| Reference Counting | O(1) per operation | O(n) for ref counts | Fails on cycles |
| Generational GC | O(young) + O(old) amortized | O(n) for all generations | Optimizes for short-lived objects |
| Copying GC | O(live objects only) | 2x space (from/to spaces) | Compacts memory |
| Incremental GC | O(k) per increment, k < n | O(n) total | Reduces pause times |
| Operation | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Heap snapshot | O(n) for all objects | O(n) snapshot size | Can be large |
| Snapshot comparison | O(n) | O(2n) for two snapshots | Shows deltas |
| Memory monitoring | O(1) per sample | O(samples) | Track over time |
| Event listener audit | O(listeners) | O(1) | Check for leaks |
| Concept | Key Takeaway |
|---|---|
| What is GC? | Automatic memory management that reclaims unreachable objects |
| How it works | Mark-and-sweep: trace from roots, mark reachable, sweep garbage |
| Reachability | Objects reachable from roots (globals, stack) are kept |
| Generational GC | Young gen (fast, frequent) + old gen (slow, rare) = optimized |
| Memory leaks | Unintentional references prevent GC (timers, listeners, closures) |
| Weak references | WeakMap/WeakSet don't prevent GC β perfect for caches |
| Detection | DevTools snapshots, performance.memory, look for growth patterns |
| Prevention | Cleanup timers/listeners, limit caches, avoid global accumulation |
Test your understanding with 3 quick questions