Performance work without measurement wastes time; the profiler shows exactly which components render, why, and for how long.
Measure before optimising, and measure a production build (npm run build && npm run preview): development mode is much slower because of extra checks and Strict Mode double rendering.
The React DevTools Profiler (browser extension) records interactions. Start recording, perform the slow action, stop, and inspect each commit:
- The flame graph shows which components rendered and how long each took; grey ones didn't render.
- The ranked view sorts by self time, pointing at the expensive component.
- Enable "Record why each component rendered" to see whether it was a state change, a parent render, changed props (and which), or a context change.
- "Highlight updates when components render" visually flashes re-rendering components while you use the app.
Typical findings and fixes:
- A keystroke re-renders the whole page because state lives too high: move it down, or pass static parts as
children. - A memoized child re-renders because of an unstable prop: stabilise it (or adopt the React Compiler, which DevTools marks with a "Memo" badge).
- A single slow component: memoize the calculation, virtualise a long list (TanStack Virtual, react-window), or move the work off the urgent path with
useTransition/useDeferredValue.
The browser's Performance panel complements this; React 19.2 adds custom tracks there showing scheduler priorities and component render timings. For production monitoring, the <Profiler id onRender> component reports render durations programmatically (it's disabled in production builds unless you use the profiling build). Also look beyond React: large bundles (split with lazy), layout thrashing and slow network waterfalls are frequent culprits.
Example
import { Profiler, useState } from "react";
function onRender(id, phase, actualDuration, baseDuration) {
// phase: "mount" | "update" | "nested-update"
// actualDuration: ms spent rendering this commit (with memoization)
// baseDuration: estimated ms to re-render the whole subtree without memoization
if (actualDuration > 16) {
console.warn(`${id} ${phase} took ${actualDuration.toFixed(1)}ms (base ${baseDuration.toFixed(1)}ms)`);
}
}
function SlowList({ n }) {
const items = Array.from({ length: n }, (_, i) => <li key={i}>Row {i}</li>);
return <ul>{items}</ul>;
}
export default function App() {
const [n, setN] = useState(1000);
return (
<Profiler id="SlowList" onRender={onRender}>
<button onClick={() => setN(x => x + 5000)}>More rows</button>
<SlowList n={n} />
</Profiler>
);
}Remember
- Profile production builds; development mode is deliberately slower
- DevTools Profiler shows what rendered, how long, and why
- Fix causes: state placement, unstable props, list virtualisation, transitions, code splitting
Watch out for
- Optimising based on development-mode timings or console logs instead of a profile.