Back to Blog

React Performance Patterns That Actually Matter

Practical performance optimization techniques for React applications, focused on real-world scenarios.

ReactPerformanceJavaScript

Performance optimization in React is often misunderstood. People reach for useMemo and React.memo everywhere, when the real gains come from architectural decisions. Here's what actually matters.

The Biggest Performance Wins

1. Colocate State

The number one cause of unnecessary re-renders is state that lives too high in the component tree:

// ❌ Bad: State in parent causes all children to re-render
function App() {
  const [filter, setFilter] = useState('');
  return (
    <>
      <SearchInput value={filter} onChange={setFilter} />
      <ExpensiveComponent /> {/* Re-renders on every keystroke! */}
      <FilteredList filter={filter} />
    </>
  );
}

// ✅ Good: Colocate state with the components that use it
function App() {
  return (
    <>
      <SearchSection /> {/* Contains its own state */}
      <ExpensiveComponent /> {/* Never re-renders unnecessarily */}
    </>
  );
}

2. Use Children Pattern

Pass components as children to avoid re-rendering them:

// ❌ Bad: ChildComponent re-renders when parent state changes
function Parent() {
  const [count, setCount] = useState(0);
  return (
    <div onClick={() => setCount(c => c + 1)}>
      <ChildComponent />
    </div>
  );
}

// ✅ Good: Children don't re-render
function Parent({ children }) {
  const [count, setCount] = useState(0);
  return (
    <div onClick={() => setCount(c => c + 1)}>
      {children}
    </div>
  );
}

// Usage
<Parent>
  <ChildComponent />
</Parent>

3. Virtualize Long Lists

For lists with many items, virtualization is essential:

import { useVirtualizer } from '@tanstack/react-virtual';

function VirtualList({ items }) {
  const parentRef = useRef(null);

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

  return (
    <div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
      <div style={{ height: virtualizer.getTotalSize() }}>
        {virtualizer.getVirtualItems().map(virtualRow => (
          <div
            key={virtualRow.key}
            style={{
              position: 'absolute',
              top: virtualRow.start,
              height: virtualRow.size,
            }}
          >
            {items[virtualRow.index]}
          </div>
        ))}
      </div>
    </div>
  );
}

When to Actually Use memo and useMemo

Use React.memo when:

  • The component renders often with the same props
  • The component is expensive to render
  • You've measured and confirmed it helps

Use useMemo when:

  • Creating objects/arrays passed to memoized children
  • Expensive calculations that don't need to run every render
// Actually useful useMemo
const sortedItems = useMemo(
  () => items.sort((a, b) => a.date - b.date),
  [items]
);

// Probably not useful
const doubled = useMemo(() => count * 2, [count]); // Multiplication is cheap

Measure First

Always use React DevTools Profiler before optimizing. You might be surprised where the actual bottlenecks are.

import { Profiler } from 'react';

function onRender(id, phase, actualDuration) {
  console.log({ id, phase, actualDuration });
}

<Profiler id="MyComponent" onRender={onRender}>
  <MyComponent />
</Profiler>

Conclusion

The best React performance optimizations are architectural:

  1. Keep state close to where it's used
  2. Split components at natural boundaries
  3. Use virtualization for long lists
  4. Only reach for memo/useMemo when you have measured proof

What performance patterns have you found most effective?