React Profiler
Optimizing React performance starts with measurement, not guesswork. Before you sprinkle memo and useMemo everywhere, you need to know which components actually render too often and how long their renders take. React ships two complementary tools for this: the Profiler tab in React DevTools for interactive exploration, and the <Profiler> component for programmatic timing. Together they turn vague “the app feels slow” complaints into precise, actionable numbers.
The DevTools Profiler
The Profiler lives in the React DevTools browser extension alongside the Components tab. To capture data, click the record button (the blue circle), interact with your app — type in a field, open a menu, navigate a route — then stop recording. DevTools breaks the session into a series of commits: each commit is one batch of DOM updates React flushed to the screen.
For accurate timings, profile a development build with the profiling-enabled React or a production build that includes the profiler. A plain dev build adds overhead that inflates numbers, so treat absolute milliseconds as relative signals, and prefer react-dom/profiling in production for trustworthy figures.
Tip: Enable “Record why each component rendered” in the Profiler settings (the gear icon) before recording. It is the single most useful setting for hunting wasted renders.
Reading the views
Each commit can be inspected through several views. The toolbar lets you step through commits one at a time, and the bars at the top show how expensive each commit was — tall, slow commits are your prime suspects.
| View | What it shows | Best for |
|---|---|---|
| Flamegraph | The component tree for one commit, widths scaled to render time | Seeing which subtree dominated a commit |
| Ranked | A flat list of components sorted by render duration | Quickly spotting the single slowest component |
| Timeline | Scheduling, suspense, and events over the whole session | Understanding concurrent work and long tasks |
In the flamegraph, gray (desaturated) bars are components that did not re-render in that commit, while colored bars did. A wide yellow or orange bar is a component that took a long time relative to its siblings. Click any bar to see its props, state, and — if you enabled the setting — exactly why it rendered.
Why did it render?
When “Record why each component rendered” is on, selecting a component in the right panel shows a line like “Props changed: (user, onSelect)” or “Hook 1 changed”. This is how you catch the classic mistake of passing a freshly-created object or inline arrow function as a prop, which breaks React.memo and forces a re-render every time the parent renders.
Output:
ProductList
Why did this render?
• Props changed: (filters)
• The parent component rendered
Seeing “Props changed: (filters)” when filters should be stable points you straight at a useMemo opportunity in the parent.
The Profiler component
The <Profiler> component measures render cost programmatically — useful for CI checks, custom dashboards, or logging timings in environments where the DevTools extension is not available. Wrap any part of the tree and supply an id and an onRender callback.
import { Profiler } from 'react';
function onRender(id, phase, actualDuration, baseDuration, startTime, commitTime) {
console.log(`[${id}] ${phase} took ${actualDuration.toFixed(2)}ms`);
}
export default function App() {
return (
<Profiler id="Dashboard" onRender={onRender}>
<Dashboard />
</Profiler>
);
}
You can nest <Profiler> components to measure subtrees independently, and each fires its own callback per commit.
The onRender arguments
The callback receives six arguments that describe a single commit of the profiled subtree.
| Argument | Meaning |
|---|---|
id | The id prop you passed, identifying which tree committed |
phase | "mount", "update", or "nested-update" |
actualDuration | Time spent rendering this commit (ms) — lower after memoization |
baseDuration | Estimated time to render the whole subtree without memoization |
startTime | When React began rendering this commit |
commitTime | When React committed this commit (shared across Profilers in the same commit) |
The most important number is actualDuration versus baseDuration. If actualDuration stays close to baseDuration across updates, your memoization is not helping; if it drops well below, your bailouts are working.
function logSlowCommits(id, phase, actualDuration) {
if (actualDuration > 16) {
// Anything over one 60fps frame budget is worth investigating.
console.warn(`Slow commit in ${id} (${phase}): ${actualDuration.toFixed(1)}ms`);
}
}
Warning:
<Profiler>adds CPU and memory overhead. Use it deliberately around suspect areas rather than wrapping your entire app permanently in production, and gate it behind a flag if you ship it.
Finding hot spots
A repeatable workflow turns the raw data into fixes. Record a representative interaction, switch to the Ranked view to find the slowest component, then check its commit count by stepping through commits — a cheap component that renders 50 times can hurt as much as one expensive render. Use the “why did it render” reason to decide the fix: unstable props call for useMemo/useCallback, frequent identical renders call for React.memo, and a single heavy computation calls for memoizing the calculation itself.
Best Practices
- Always measure first — let the Profiler tell you where the time goes before adding memoization.
- Turn on “Record why each component rendered” to distinguish necessary renders from wasted ones.
- Profile a production or profiling-enabled build for trustworthy numbers; treat dev-build timings as relative.
- Watch both render duration and render count — many cheap renders can be as costly as one slow one.
- Compare
actualDurationagainstbaseDurationto confirm your optimizations actually pay off. - Keep
<Profiler>wrappers scoped to suspect areas and behind a flag, since they add overhead. - Re-profile after each change to verify the fix helped and did not move the bottleneck elsewhere.