Front-end Engineering Lab

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 objects

Detecting 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

  1. Always cleanup - useEffect return function
  2. Avoid global references - Component-scoped only
  3. WeakMap for caching - Allows garbage collection
  4. Profile regularly - Catch leaks early
  5. Test mount/unmount - Verify cleanup works
  6. Use React DevTools Profiler - Check for retained components
  7. Monitor in production - Track memory metrics
  8. Clear timers/intervals - Never forget cleanup

Memory leaks accumulate over time—always clean up resources in useEffect!

On this page