React is fast by default — until it isn't. Most performance problems aren't React's fault; they come from doing more work than necessary. Here's the checklist I run through when an app starts feeling sluggish.
1. Measure before you optimize
Open the React Profiler and record an interaction. You'll usually find that one or two components are re-rendering far more than they should. Don't guess — let the flame graph point you at the problem.
2. Stop unnecessary re-renders
A component re-renders when its state or props change. Two common culprits create new references on every render:
- Inline object/array literals passed as props
- Inline functions passed to memoized children
Stabilize them with useMemo and useCallback, and
wrap pure presentational components in React.memo:
const handleSelect = useCallback((id) => {
setSelected(id);
}, []);
const sorted = useMemo(
() => items.slice().sort(byName),
[items]
);
3. Don't lift state higher than it needs to be
State that lives too high re-renders the whole tree on every keystroke. Keep state as local as possible, and split large contexts so unrelated consumers don't re-render together.
4. Virtualize long lists
Rendering 10,000 rows kills performance even if each row is cheap. Render
only what's visible with a windowing library like
react-window. The DOM stays small and scrolling stays smooth.
5. Trim the bundle
A fast render doesn't help if the user waited five seconds for the
JavaScript to download. Code-split routes with React.lazy and
Suspense, audit your dependencies, and ship less:
const Dashboard = React.lazy(() => import('./Dashboard'));
<Suspense fallback={<Spinner />}>
<Dashboard />
</Suspense>
The goal isn't to memoize everything — it's to do less work. Measure, fix the biggest offender, measure again.
Wrapping up
Profile first, eliminate wasted renders, keep state local, virtualize big lists, and ship a smaller bundle. Do those five things and the vast majority of React performance issues simply disappear.