React Performance: Fix the Things That Actually Matter
A diagnosis-first guide to React performance — how to find the real bottleneck with the Profiler, and the five fixes that account for most real-world wins.
Table of contents
- 1. Too many components re-render
- 2. A long list is rendering every row
- 3. Context is re-rendering everything
- 4. Expensive work on the render path
- 5. The bundle is too large
- Measure in production, not just locally
- Frequently asked questions
- Should I use the React Compiler?
- Is the virtual DOM the bottleneck?
- How many re-renders is too many?
- Does Strict Mode's double render hurt performance?
- Related reading
- References
Most React performance work is wasted because it starts with a guess. Start with a measurement instead: open the React DevTools Profiler, record the slow interaction, and look at what actually took time.
You will almost always find one of five things.
1. Too many components re-render#
The Profiler flamegraph shows which components rendered and why. A common finding: typing in one input re-renders a 200-row table.
The best fix is structural — move state closer to where it is used:
// Before: every keystroke re-renders Table
function Page() {
const [query, setQuery] = useState('');
return <><input value={query} onChange={...} /><Table rows={rows} /></>;
}
// After: the input owns its own state
function Page() {
return <><SearchInput /><Table rows={rows} /></>;
}Only if that is impossible reach for memo — and then remember that memo needs stable props to do anything. See useMemo and useCallback.
2. A long list is rendering every row#
Rendering 5,000 DOM rows is slow no matter how well your components are written. The fix is virtualisation: render only the ~30 rows in the viewport.
This is usually the single largest available win in a data-heavy app, and it is not something memoisation can approximate.
3. Context is re-rendering everything#
Every consumer of a context re-renders when the provider's value changes — including consumers that only read one field of it.
// Every consumer re-renders whenever anything in the app state changes
<AppContext.Provider value={{ user, theme, cart, notifications }}>Two fixes: split into separate contexts by update frequency, and memoise each value.
<ThemeContext.Provider value={themeValue}> {/* rarely changes */}
<CartContext.Provider value={cartValue}> {/* changes often */}4. Expensive work on the render path#
Anything synchronous in a render body blocks paint. Sorting, filtering and formatting large collections belong in useMemo — or better, on the server.
// Server Component: the sort happens at build/request time, not in the browser
const sorted = rows.toSorted((a, b) => b.score - a.score);Moving work to a Server Component removes it from the client entirely, which beats memoising it.
5. The bundle is too large#
Slow first load is a different problem from slow interaction, and memoisation does nothing for it. Look at:
- Route-level splitting. In the App Router this is automatic per route.
- Barrel imports.
import { Slot } from "radix-ui"can pull an entire component library into a chunk because the barrel defeats tree-shaking. Next'soptimizePackageImportsfixes this, and the effect can be dramatic — we measured a single<Badge>costing 76 kB of First Load JS on this site until it was configured. - Heavy dependencies behind a dynamic import. A syntax highlighter, a chart library or a rich-text editor should load when used, not on page load.
const Editor = dynamic(() => import('./editor'), { ssr: false });Measure in production, not just locally#
Your machine is faster than your users' machines. Two things worth doing:
- Throttle the CPU 4x in devtools while profiling. It changes what looks slow.
- Collect real-user Interaction to Next Paint (INP) with
web-vitals. Lab numbers and field numbers diverge, and the field numbers are the ones Google uses.
Frequently asked questions#
Should I use the React Compiler?#
If you can, yes. It applies memoisation automatically and correctly, which removes an entire category of manual optimisation and the bugs that come with getting dependency arrays wrong.
Is the virtual DOM the bottleneck?#
Almost never. Diffing is fast; the expensive parts are your component bodies and the resulting DOM mutations.
How many re-renders is too many?#
There is no number. A re-render that produces no DOM change is cheap. The question is always whether the interaction feels slow and what the Profiler attributes it to.
Does Strict Mode's double render hurt performance?#
Only in development, and only to surface impure render logic. Production renders once.
Related reading#
- The JavaScript Event Loop — why long tasks block input
- Core Web Vitals
- React key Prop