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 collectedCommon 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 existPerformance 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 growthProgrammatic 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 foreverLazy 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.