DEV Community

Cover image for React Performance Patterns I Use Every Day
Monalisa Das
Monalisa Das

Posted on Originally published at monalisadas-knowme.vercel.app

React Performance Patterns I Use Every Day

After profiling dozens of React apps, the same culprits appear again and again. Here are the patterns I reach for first — and the ones that look smart but actually hurt.

Performance optimization in React is one of those areas where intuition frequently leads you astray. Developers reach for useMemo everywhere, wrap every function in useCallback, and then wonder why their app is slower. Let me share what actually moves the needle.

1. Colocate State as Low as Possible

The single biggest source of unnecessary re-renders is state that lives too high in the tree. If only one child needs a piece of state, keep it in that child. Every time that state updates, only that subtree re-renders.

// Bad: hover state lives at App level -- every hover event re-renders the entire tree
function App() {
  const [hovered, setHovered] = useState(false)
  return <HugeTree><Button onHover={setHovered} /></HugeTree>
}

// Good: hover state is local to Button -- only Button re-renders on hover
// HugeTree is completely unaffected. This is the fix that costs zero effort
function Button() {
  const [hovered, setHovered] = useState(false)
  return <button onMouseEnter={() => setHovered(true)} />
}
Enter fullscreen mode Exit fullscreen mode

2. Use Refs for Values That Don't Drive UI

Not all values need to be state. If a value changes frequently but doesn't need to trigger a re-render, use a ref. The classic example is storing the previous value of a prop, or tracking animation frame IDs.

// Canvas animation: mouse position changes 60 times/second
// If this were useState, React would try to re-render 60 times/second
function Canvas() {
  const mousePos = useRef({ x: 0, y: 0 }) // Ref: mutation doesn't schedule a render
  const canvasRef = useRef<HTMLCanvasElement>(null)

  useEffect(() => {
    const onMove = (e: MouseEvent) => {
      mousePos.current = { x: e.clientX, y: e.clientY }
      // Draw directly to canvas -- zero React involvement, zero re-renders
      draw(canvasRef.current!, mousePos.current)
    }
    window.addEventListener('mousemove', onMove)
    return () => window.removeEventListener('mousemove', onMove)
  }, []) // Empty deps: we read mousePos.current at event time, not render time
}
Enter fullscreen mode Exit fullscreen mode

3. Memo Boundaries, Not Individual Values

Instead of sprinkling useMemo everywhere, identify the expensive component subtrees and wrap them with React.memo. Then ensure their props are stable — that's where useMemo and useCallback actually pay off.

4. Virtualize Long Lists

If you're rendering more than ~100 rows, virtualize. @tanstack/virtual is my go-to — it's framework-agnostic, tiny, and handles variable-height rows elegantly. The DOM node count drops from thousands to ~20 regardless of list length.

Run the demos — see colocated state and refs eliminate re-renders without useMemo everywhere

The order matters: colocate state first, it's free and usually fixes 80% of the problem. Only then measure — React DevTools Profiler will tell you exactly which components re-render on each update. Don't reach for useMemo until you've measured where time is actually going.

Top comments (0)