A Practical Guide to React Performance
Most React performance problems come from a handful of recurring patterns. This guide covers the techniques that deliver the biggest wins with the least complexity.
React is fast by default, until it isn't. The good news is that the vast majority of real-world performance issues trace back to a small set of patterns. Fix those, and you rarely need exotic optimizations.
Measure before you optimize
The first rule of performance work is to never guess. Use the React Profiler and the browser's performance panel to find what actually renders, and how often.
Premature optimization
Wrapping every component in memo and every value in
useMemo adds complexity and can make things slower. Optimize the
hot paths you have measured, not the ones you imagine.
Avoid unnecessary re-renders
A re-render isn't inherently bad, but cascading re-renders of expensive subtrees are. The most common culprit is passing a freshly-created object or function on every render.
// ❌ A new array + handler every render breaks memoized children
function ProductList({ products }) {
return (
<List
items={products.filter((p) => p.inStock)}
onSelect={(id) => track(id)}
/>
);
}
// ✅ Stabilize derived data and callbacks
function ProductList({ products }) {
const inStock = useMemo(
() => products.filter((p) => p.inStock),
[products],
);
const handleSelect = useCallback((id) => track(id), []);
return <List items={inStock} onSelect={handleSelect} />;
}Memoize the right things
React.memo, useMemo and useCallback are tools for keeping referential
identity stable across renders. Reach for them when:
- a child component is expensive to render, and
- it receives props that would otherwise change identity every render.
Better still, let the React Compiler handle memoization for you. Adding it is a single dependency:
npm install babel-plugin-react-compilerShip less JavaScript
The fastest code is the code you never send. Code-splitting and lazy loading keep the initial bundle small.
import { lazy, Suspense } from 'react';
const Editor = lazy(() => import('./Editor'));
export function Panel() {
return (
<Suspense fallback={<Spinner />}>
<Editor />
</Suspense>
);
}Move work to the server
With React Server Components, data fetching and heavy rendering can happen on the server, shipping only the resulting HTML and a minimal amount of client JS. The practical heuristic:
| Concern | Prefer |
|---|---|
| Data fetching | Server Component |
| Static content | Server Component |
| Interactivity/state | Client Component ('use client') |
| Browser-only APIs | Client Component |
Optimize images and fonts
Images are usually the largest assets on a page. Use a real image pipeline (responsive sizes, modern formats, lazy loading) and load only the font weights you actually use.
The short version
Measure first. Cut re-renders on hot paths. Ship less JavaScript. Push work to the server. Optimize media. Do those five and you've handled 90% of real React performance problems.