PatternsObservability
Memory Leak Detection
Find and fix memory leaks in React applications
Memory leaks cause apps to slow down and crash. Learn to detect and fix common patterns.
Common Causes
// ❌ Leak: Event listener not removed
useEffect(() => {
window.addEventListener('resize', handleResize);
// Missing cleanup!
}, []);
// ✅ Fixed: Cleanup function
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// ❌ Leak: Timer not cleared
useEffect(() => {
setInterval(() => updateData(), 1000);
}, []);
// ✅ Fixed: Clear timer
useEffect(() => {
const id = setInterval(() => updateData(), 1000);
return () => clearInterval(id);
}, []);
// ❌ Leak: setState after unmount
useEffect(() => {
fetchData().then(data => setState(data)); // Component might unmount!
}, []);
// ✅ Fixed: Cancel on unmount
useEffect(() => {
let cancelled = false;
fetchData().then(data => {
if (!cancelled) setState(data);
});
return () => { cancelled = true; };
}, []);Chrome DevTools Memory Profiler
1. Open DevTools → Memory tab
2. Take Heap Snapshot
3. Interact with app (add/remove components)
4. Take another Heap Snapshot
5. Compare snapshots
6. Look for unexpected retained objectsDetecting Leaks Programmatically
// Monitor memory usage
export function useMemoryMonitor() {
useEffect(() => {
if (!('memory' in performance)) return;
const checkMemory = () => {
const memory = (performance as any).memory;
const used = memory.usedJSHeapSize / 1048576; // MB
const total = memory.totalJSHeapSize / 1048576;
console.log(`Memory: ${used.toFixed(2)}MB / ${total.toFixed(2)}MB`);
if (used / total > 0.9) {
console.warn('High memory usage!');
}
};
const interval = setInterval(checkMemory, 5000);
return () => clearInterval(interval);
}, []);
}Common Patterns
Closure Leaks
// ❌ Leak: Closure captures large array
function Component() {
const largeData = Array(1000000).fill('data');
const handleClick = () => {
console.log(largeData.length); // Captures entire array!
};
return <button onClick={handleClick}>Click</button>;
}
// ✅ Fixed: Only capture what's needed
function Component() {
const largeData = Array(1000000).fill('data');
const dataLength = largeData.length;
const handleClick = () => {
console.log(dataLength); // Only captures number
};
return <button onClick={handleClick}>Click</button>;
}DOM References
// ❌ Leak: Storing DOM references
const cache = new Map();
function Component({ id }: Props) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
cache.set(id, ref.current); // Prevents GC!
}, [id]);
return <div ref={ref} />;
}
// ✅ Fixed: Clean up references
useEffect(() => {
cache.set(id, ref.current);
return () => cache.delete(id);
}, [id]);Global Event Listeners
// ❌ Leak: Global listener references component
function Component() {
const [data, setData] = useState();
useEffect(() => {
window.addEventListener('storage', (e) => {
setData(e.newValue); // Keeps component in memory!
});
}, []);
return <div>{data}</div>;
}
// ✅ Fixed: Remove listener
useEffect(() => {
const handler = (e: StorageEvent) => {
setData(e.newValue);
};
window.addEventListener('storage', handler);
return () => window.removeEventListener('storage', handler);
}, []);Testing for Leaks
// Automated leak detection
describe('Component memory leaks', () => {
it('cleans up on unmount', async () => {
const { unmount } = render(<Component />);
// Take snapshot
const before = (performance as any).memory?.usedJSHeapSize;
// Mount/unmount 100 times
for (let i = 0; i < 100; i++) {
const { unmount } = render(<Component />);
unmount();
}
// Force GC (only in test environment)
if (global.gc) global.gc();
// Take snapshot
const after = (performance as any).memory?.usedJSHeapSize;
// Memory should not grow significantly
expect(after - before).toBeLessThan(1000000); // 1MB threshold
});
});Best Practices
- Always cleanup - useEffect return function
- Avoid global references - Component-scoped only
- WeakMap for caching - Allows garbage collection
- Profile regularly - Catch leaks early
- Test mount/unmount - Verify cleanup works
- Use React DevTools Profiler - Check for retained components
- Monitor in production - Track memory metrics
- Clear timers/intervals - Never forget cleanup
Memory leaks accumulate over time—always clean up resources in useEffect!