perf hacks

Memory Management

Prevent memory leaks and optimize memory usage in React applications

Memory issues cause performance degradation over time, leading to sluggish interactions and crashes. Understanding JavaScript and React memory patterns is essential for building performant applications.

Common Memory Leaks

Uncleared Intervals and Timeouts

// ❌ Memory leak: interval never cleared
function Component() {
  useEffect(() => {
    setInterval(() => {
      updateData();
    }, 1000);
  }, []);
}

// ✅ Fixed: Clear interval on cleanup
function Component() {
  useEffect(() => {
    const intervalId = setInterval(() => {
      updateData();
    }, 1000);

    return () => clearInterval(intervalId);
  }, []);
}

Event Listeners

// ❌ Memory leak: listener never removed
function Component() {
  useEffect(() => {
    window.addEventListener('resize', handleResize);
  }, []);
}

// ✅ Fixed: Remove listener on cleanup
function Component() {
  useEffect(() => {
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);
}

Subscriptions

// ❌ Memory leak: subscription never unsubscribed
function Component() {
  useEffect(() => {
    const subscription = dataSource.subscribe(data => {
      setData(data);
    });
  }, []);
}

// ✅ Fixed: Unsubscribe on cleanup
function Component() {
  useEffect(() => {
    const subscription = dataSource.subscribe(data => {
      setData(data);
    });

    return () => subscription.unsubscribe();
  }, []);
}

Abort Controllers

// ❌ Memory leak: fetch continues after unmount
function Component({ id }) {
  useEffect(() => {
    fetch(`/api/data/${id}`)
      .then(r => r.json())
      .then(data => setData(data));
  }, [id]);
}

// ✅ Fixed: Abort fetch on cleanup
function Component({ id }) {
  useEffect(() => {
    const controller = new AbortController();

    fetch(`/api/data/${id}`, { signal: controller.signal })
      .then(r => r.json())
      .then(data => setData(data))
      .catch(e => {
        if (e.name !== 'AbortError') throw e;
      });

    return () => controller.abort();
  }, [id]);
}

Closures and References

Stale Closures

// ❌ Problem: Stale closure captures old value
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1); // Always uses initial count (0)
    }, 1000);
    return () => clearInterval(id);
  }, []); // Missing count dependency
}

// ✅ Fixed: Use functional update
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1); // Uses current value
    }, 1000);
    return () => clearInterval(id);
  }, []);
}

Callback References

// ❌ Problem: Callback keeps reference to old data
function List({ items }) {
  const [selected, setSelected] = useState(null);

  const handleSelect = (id) => {
    const item = items.find(i => i.id === id); // Captures items
    setSelected(item);
  };

  return items.map(item => (
    <Item key={item.id} onClick={() => handleSelect(item.id)} />
  ));
}

// ✅ Fixed: Pass item directly or use ref
function List({ items }) {
  const itemsRef = useRef(items);
  itemsRef.current = items;

  const handleSelect = useCallback((id) => {
    const item = itemsRef.current.find(i => i.id === id);
    setSelected(item);
  }, []);
}

Large Data Handling

Virtualization

// ❌ Bad: All items in memory and DOM
function HugeList({ items }) {
  return items.map(item => <ListItem key={item.id} {...item} />);
}

// ✅ Good: Only visible items in DOM
import { useVirtualizer } from '@tanstack/react-virtual';

function HugeList({ items }) {
  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50,
  });

  return (
    <div ref={parentRef}>
      {virtualizer.getVirtualItems().map(row => (
        <ListItem key={row.key} {...items[row.index]} />
      ))}
    </div>
  );
}

Pagination

// Load data in pages instead of all at once
function PaginatedList() {
  const [page, setPage] = useState(1);
  const { data, isLoading } = useQuery({
    queryKey: ['items', page],
    queryFn: () => fetchPage(page),
    keepPreviousData: true,
  });

  return (
    <div>
      {data?.items.map(item => <Item key={item.id} {...item} />)}
      <Pagination page={page} onPageChange={setPage} />
    </div>
  );
}

Clearing Unused Data

// Clear data when navigating away
function ProductPage({ productId }) {
  const [product, setProduct] = useState(null);
  const [reviews, setReviews] = useState([]);

  useEffect(() => {
    loadProduct(productId).then(setProduct);
    loadReviews(productId).then(setReviews);

    return () => {
      // Clear heavy data on unmount
      setProduct(null);
      setReviews([]);
    };
  }, [productId]);
}

WeakMap and WeakRef

WeakMap for Caching

// Cache that allows garbage collection
const cache = new WeakMap<object, ProcessedData>();

function processObject(obj: object): ProcessedData {
  if (cache.has(obj)) {
    return cache.get(obj)!;
  }

  const result = expensiveProcessing(obj);
  cache.set(obj, result);
  return result;
}

WeakRef for Optional References

class Cache {
  private cache = new Map<string, WeakRef<object>>();

  set(key: string, value: object) {
    this.cache.set(key, new WeakRef(value));
  }

  get(key: string): object | undefined {
    const ref = this.cache.get(key);
    return ref?.deref();
  }
}

Profiling Memory

Chrome DevTools Memory Panel

Open DevTools → Memory tab

Select "Heap snapshot"

Take snapshot before the action

Perform the action you're investigating

Take snapshot after

Compare snapshots using the dropdown to find retained objects

Programmatic Memory Monitoring

// Monitor memory usage
function logMemory() {
  if ('memory' in performance) {
    const memory = (performance as any).memory;
    console.log({
      usedJSHeapSize: formatBytes(memory.usedJSHeapSize),
      totalJSHeapSize: formatBytes(memory.totalJSHeapSize),
      jsHeapSizeLimit: formatBytes(memory.jsHeapSizeLimit),
    });
  }
}

function formatBytes(bytes: number): string {
  return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
}

React-Specific Patterns

useRef for Non-Reactive Data

// Store data that doesn't need re-renders
function Component() {
  const cache = useRef(new Map());
  const lastValue = useRef(null);

  // These don't cause re-renders
  cache.current.set('key', 'value');
  lastValue.current = computedValue;
}

Cleanup in Custom Hooks

function useWebSocket(url: string) {
  const [data, setData] = useState(null);
  const wsRef = useRef<WebSocket | null>(null);

  useEffect(() => {
    wsRef.current = new WebSocket(url);

    wsRef.current.onmessage = (event) => {
      setData(JSON.parse(event.data));
    };

    return () => {
      wsRef.current?.close();
      wsRef.current = null;
    };
  }, [url]);

  return data;
}

Checklist

Effect Cleanup

Clear intervals and timeouts Store IDs and call clearInterval/clearTimeout in cleanup.

Remove event listeners Always remove listeners added to window/document.

Unsubscribe from subscriptions Call unsubscribe on any observable or event emitter.

Abort pending fetches Use AbortController to cancel in-flight requests.

Close WebSocket connections Call close() on cleanup to prevent orphaned connections.

Data Management

Virtualize long lists Only render visible items to reduce DOM size.

Paginate large datasets Load data in chunks instead of all at once.

Clear data on navigation Set state to null when unmounting data-heavy components.

Use WeakMap for caches Allows garbage collection of cached objects.

References

Avoid stale closures Use functional updates or refs for current values.

Use refs for mutable values Store non-reactive data in useRef to avoid re-renders.

Clear refs on cleanup Set ref.current to null in cleanup functions.

Use WeakRef when appropriate For optional references that shouldn't prevent GC.

Monitoring

Profile with DevTools Use Chrome's Memory panel to identify leaks.

Take heap snapshots Compare before/after snapshots to find retained objects.

Compare memory over time Watch for gradual increases during usage.

Test on memory-constrained devices Mobile devices expose memory issues faster.


For advanced memory forensics with heap snapshots and retainer trees, see DevTools Debugging.

On this page