Back to Blog
Memory ManagementJavaScriptPerformanceDebugging

Memory Management in JavaScript: Finding and Fixing Leaks

Understand how JavaScript manages memory. Find memory leaks, use profiling tools, and write memory-efficient code.

B
Bootspring Team
Engineering
December 12, 2024
7 min read

Memory leaks cause applications to slow down and eventually crash. Understanding how JavaScript manages memory helps you write efficient code and fix issues quickly.

How JavaScript Memory Works#

The Memory Lifecycle#

1. Allocation → Memory is allocated when you create objects 2. Use → Read and write to allocated memory 3. Release → Memory is released when no longer needed JavaScript handles allocation and release automatically, but you control what objects are created and referenced.

Garbage Collection#

1// Mark-and-sweep algorithm 2// Objects reachable from roots are "alive" 3// Unreachable objects are garbage collected 4 5// Roots include: 6// - Global objects (window, global) 7// - Currently executing functions 8// - Variables in the call stack 9 10function example() { 11 const obj = { data: 'hello' }; // Allocated 12 console.log(obj.data); // Used 13} // Released (obj no longer reachable) 14 15example(); 16// After function ends, obj is garbage collected

Common Memory Leaks#

Accidental Globals#

1// ❌ Accidental global (missing 'const'/'let') 2function processData() { 3 results = []; // Creates global variable! 4 for (let i = 0; i < 10000; i++) { 5 results.push(expensiveComputation(i)); 6 } 7} 8 9// Each call adds more data, never released 10processData(); 11processData(); 12processData(); 13 14// ✅ Fix: Use proper declarations 15function processData() { 16 const results = []; 17 for (let i = 0; i < 10000; i++) { 18 results.push(expensiveComputation(i)); 19 } 20 return results; 21}

Forgotten Timers#

1// ❌ Timer holds reference forever 2function startUpdates() { 3 const data = fetchLargeDataset(); 4 5 setInterval(() => { 6 updateUI(data); // data cannot be garbage collected 7 }, 1000); 8} 9 10// ✅ Fix: Clear timers when done 11function startUpdates() { 12 const data = fetchLargeDataset(); 13 14 const intervalId = setInterval(() => { 15 updateUI(data); 16 }, 1000); 17 18 // Clear when component unmounts or user navigates away 19 return () => clearInterval(intervalId); 20}

Detached DOM Elements#

1// ❌ DOM elements referenced in JavaScript 2let elements = []; 3 4function createElements() { 5 for (let i = 0; i < 1000; i++) { 6 const div = document.createElement('div'); 7 div.innerHTML = `Item ${i}`; 8 document.body.appendChild(div); 9 elements.push(div); // Reference retained 10 } 11} 12 13function removeElements() { 14 document.body.innerHTML = ''; 15 // DOM elements removed, but 'elements' array still holds references 16 // They cannot be garbage collected! 17} 18 19// ✅ Fix: Clear references 20function removeElements() { 21 document.body.innerHTML = ''; 22 elements = []; // Clear references 23}

Closures Holding References#

1// ❌ Closure retains large object 2function createHandler() { 3 const largeData = new Array(1000000).fill('data'); 4 5 return function handler() { 6 console.log(largeData.length); // Closure holds largeData 7 }; 8} 9 10const handler = createHandler(); 11// largeData stays in memory as long as handler exists 12 13// ✅ Fix: Only capture what you need 14function createHandler() { 15 const largeData = new Array(1000000).fill('data'); 16 const length = largeData.length; // Capture only the length 17 18 return function handler() { 19 console.log(length); 20 }; 21}

Event Listeners#

1// ❌ Event listeners not removed 2class Component { 3 constructor() { 4 this.data = new Array(10000).fill('data'); 5 window.addEventListener('resize', this.handleResize.bind(this)); 6 } 7 8 handleResize() { 9 console.log('Resizing with data:', this.data.length); 10 } 11 12 // Component destroyed but listener remains 13 destroy() { 14 // Missing: remove event listener 15 } 16} 17 18// ✅ Fix: Remove listeners on cleanup 19class Component { 20 constructor() { 21 this.data = new Array(10000).fill('data'); 22 this.boundHandleResize = this.handleResize.bind(this); 23 window.addEventListener('resize', this.boundHandleResize); 24 } 25 26 handleResize() { 27 console.log('Resizing with data:', this.data.length); 28 } 29 30 destroy() { 31 window.removeEventListener('resize', this.boundHandleResize); 32 } 33}

React-Specific Leaks#

1// ❌ Subscription not cleaned up 2function UserStatus({ userId }) { 3 const [status, setStatus] = useState(null); 4 5 useEffect(() => { 6 const subscription = subscribeToStatus(userId, setStatus); 7 // Missing cleanup! 8 }, [userId]); 9 10 return <div>{status}</div>; 11} 12 13// ✅ Fix: Return cleanup function 14function UserStatus({ userId }) { 15 const [status, setStatus] = useState(null); 16 17 useEffect(() => { 18 const subscription = subscribeToStatus(userId, setStatus); 19 return () => subscription.unsubscribe(); // Cleanup 20 }, [userId]); 21 22 return <div>{status}</div>; 23} 24 25// ❌ State update on unmounted component 26function DataFetcher() { 27 const [data, setData] = useState(null); 28 29 useEffect(() => { 30 fetchData().then(result => { 31 setData(result); // Component might be unmounted! 32 }); 33 }, []); 34 35 return <div>{data}</div>; 36} 37 38// ✅ Fix: Check if mounted 39function DataFetcher() { 40 const [data, setData] = useState(null); 41 42 useEffect(() => { 43 let isMounted = true; 44 45 fetchData().then(result => { 46 if (isMounted) { 47 setData(result); 48 } 49 }); 50 51 return () => { isMounted = false; }; 52 }, []); 53 54 return <div>{data}</div>; 55}

Finding Memory Leaks#

Chrome DevTools Memory Panel#

1// Take heap snapshots to find leaks 2 3// 1. Open DevTools → Memory tab 4// 2. Take heap snapshot (baseline) 5// 3. Perform suspected leaking action 6// 4. Take another snapshot 7// 5. Compare snapshots 8 9// Look for: 10// - Growing object counts 11// - Detached DOM trees 12// - Large arrays/objects that shouldn't exist

Performance Timeline#

1// Record memory over time 2 3// 1. Open DevTools → Performance tab 4// 2. Enable Memory checkbox 5// 3. Record while using app 6// 4. Look for memory that grows and never drops 7 8// Healthy: sawtooth pattern (grows then GC drops it) 9// Leak: continuous growth

Programmatic Monitoring#

1// Monitor memory in code 2function logMemoryUsage() { 3 if (performance.memory) { 4 console.log({ 5 usedJSHeapSize: `${(performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(2)} MB`, 6 totalJSHeapSize: `${(performance.memory.totalJSHeapSize / 1024 / 1024).toFixed(2)} MB`, 7 }); 8 } 9} 10 11// Log periodically 12setInterval(logMemoryUsage, 10000);

Memory-Efficient Patterns#

Object Pooling#

1// Reuse objects instead of creating new ones 2class ObjectPool { 3 constructor(createFn, resetFn, initialSize = 10) { 4 this.createFn = createFn; 5 this.resetFn = resetFn; 6 this.pool = Array.from({ length: initialSize }, createFn); 7 } 8 9 acquire() { 10 return this.pool.pop() || this.createFn(); 11 } 12 13 release(obj) { 14 this.resetFn(obj); 15 this.pool.push(obj); 16 } 17} 18 19// Usage 20const particlePool = new ObjectPool( 21 () => ({ x: 0, y: 0, vx: 0, vy: 0, active: false }), 22 (p) => { p.x = 0; p.y = 0; p.vx = 0; p.vy = 0; p.active = false; } 23); 24 25function spawnParticle() { 26 const particle = particlePool.acquire(); 27 particle.x = Math.random() * 100; 28 particle.y = Math.random() * 100; 29 particle.active = true; 30 return particle; 31} 32 33function removeParticle(particle) { 34 particlePool.release(particle); 35}

WeakMap and WeakSet#

1// WeakMap allows garbage collection of keys 2const cache = new WeakMap(); 3 4function getCachedData(obj) { 5 if (cache.has(obj)) { 6 return cache.get(obj); 7 } 8 const data = expensiveComputation(obj); 9 cache.set(obj, data); 10 return data; 11} 12 13// When obj is no longer referenced elsewhere, 14// both obj and its cached data can be garbage collected 15 16// Regular Map would prevent garbage collection: 17const badCache = new Map(); 18badCache.set(someObject, data); 19// someObject stays in memory forever

Lazy Loading#

1// Load data only when needed 2class LazyImage { 3 constructor(url) { 4 this.url = url; 5 this._data = null; 6 } 7 8 get data() { 9 if (!this._data) { 10 this._data = loadImage(this.url); 11 } 12 return this._data; 13 } 14 15 unload() { 16 this._data = null; 17 } 18}

Node.js Specific#

1// Use --expose-gc flag to manually trigger GC 2// node --expose-gc app.js 3 4if (global.gc) { 5 global.gc(); 6} 7 8// Monitor with process.memoryUsage() 9console.log(process.memoryUsage()); 10// { 11// rss: 30000000, // Resident Set Size 12// heapTotal: 10000000, // Total heap allocated 13// heapUsed: 5000000, // Heap currently used 14// external: 1000000 // C++ objects bound to JS 15// }

Conclusion#

Memory leaks are preventable with good practices: clean up event listeners, clear timers, avoid accidental globals, and be mindful of closures. Use Chrome DevTools to find leaks, and consider patterns like object pooling for performance-critical code.

Regular profiling catches leaks early. Make it part of your development workflow, not just debugging.

Share this article

Help spread the word about Bootspring