Performance in React
Why a component re-renders, memo, useMemo, useCallback, useTransition, virtualization.
Updated
What performance means in React
A React app is usually slow for one of two reasons: it re-renders too much (components that haven't changed are recomputed) or it does too much work in one render (a list of 5,000 rows, a heavy calculation). Before any optimization, measure — with React DevTools → Profiler.
When a component re-renders
- Its own state changed.
- Its parent re-rendered (by default, all children re-render — even if the props are identical).
- A Context it reads changed its value.
A re-render does not mean a DOM change — React compares the result and touches the DOM only where there's a difference. Re-renders are usually cheap; they become a problem with big or very frequent components.
The tools — what each one does
| Tool | What it does | When |
|---|---|---|
| moving state down | only the small component re-renders | the first option, free |
children as a prop |
the passed content doesn't re-render with the parent | stateful wrappers (a modal, an accordion) |
memo(Component) |
skips re-rendering if the props are equal (Object.is) |
an expensive component, often re-rendered with the same props |
useMemo(() => calc, deps) |
keeps a calculation's result | a heavy calculation or an object passed to a memo |
useCallback(fn, deps) |
keeps the same function between renders | a function passed to a memo or used in deps |
useTransition / useDeferredValue |
marks an update as non-urgent | filtering a big list while typing — the input stays smooth |
| virtualization | renders only the visible rows | lists of thousands of items (TanStack Virtual) |
The React Compiler
The React Compiler analyzes the code at build time and automatically adds memoization (the equivalent of memo / useMemo / useCallback) where it's needed. With it enabled, you write simple code, and most manual memoization becomes unnecessary. Next.js supports it through the reactCompiler option in next.config.ts.
The condition: the code follows the rules of React — pure components, no mutating props or state.
The reference trap
const Row = memo(function Row({ item, onSelect }) { ... })
function List({ items }) {
return items.map(item => (
<Row key={item.id} item={item} onSelect={() => select(item.id)} /> // a NEW function on every render
))
}memo compares props by reference; a function or object created inline is always "new" → memo doesn't help. Hence useCallback / useMemo — or the React Compiler.
The order in which to optimize
- Measure (Profiler): which component, how long, why it re-renders.
- Move state closer to where it's used; split components.
- Server Components for everything that isn't interactive — zero cost on the client.
- Virtualization for long lists;
useTransitionfor heavy updates. - Targeted
memo/useMemo/useCallback— or turn on the React Compiler.
Summary
- A re-render = its own state, a re-rendered parent or a changed Context; measure with the Profiler.
- Structure first (state lower,
children), thenmemo+useMemo/useCallback. useTransitionfor heavy updates, virtualization for big lists, the React Compiler for automatic memoization.