Profiling React renders with the DevTools Profiler means recording a real interaction, reading the resulting flamegraph for colored versus gray bars, and asking "why did this render" before writing a single line of optimization code. Adding React.memo to a component you haven't actually profiled is a guess, and guesses are how codebases end up with a dozen memoized components that were never the problem in the first place. The Profiler turns the guess into a measurement.
Quick take: Record a session in the React DevTools Profiler tab, then read the flamegraph: colored bars are components that re-rendered, gray bars were skipped. Click a colored bar to see why it re-rendered (props changed, state changed, or a parent re-rendered). Fix the actual cause, usually an unstable prop reference, before reaching for
React.memo, which only helps once the props feeding into it are stable.
How Do You Record a Real Interaction?
Open your app, open DevTools, switch to the Profiler tab, and hit record. Then perform the exact interaction you suspect is slow, typing in a search box, opening a modal, clicking a filter. Stop recording. Don't profile a page load in isolation if the actual complaint is about typing lag, record the typing.
The Profiler gives you a commit-by-commit timeline at the top. Each vertical bar is one React commit (one batch of state updates flushed to the DOM). Click through them to find the one that corresponds to the slow interaction, usually the tallest bar if you're chasing a jank complaint. On a typical mid-size app, a single typing interaction produces somewhere between five and fifteen commits, one per keystroke that triggers a state update, so the timeline can look busier than expected the first time you record one.
How Do You Read the Flamegraph?
Selecting a commit shows a flamegraph of your component tree for that render. Two things matter here:
- Bar color: gray means the component was skipped (React decided not to re-render it, either because
React.memoblocked it or because nothing about it changed). Yellow through red means it re-rendered, with darker red taking longer. - Bar width: proportional to how long that component (and its children) took to render.
If you expected a component to be skipped and it's colored instead, that's the bug. Click it. According to the React docs, the Profiler's timing numbers reflect development-mode overhead and run measurably slower than a production build, so treat the relative ranking between components as the signal, not the absolute millisecond values, those will drop once you check against a production build.
Flamegraph is React DevTools' term for the tree-shaped visualization where each bar represents one component, nested to match the actual component tree, and colored or sized by render cost for the selected commit. It's a standard profiling visualization borrowed from general performance tooling, not something React invented, so the same reading skills transfer if you've used a flamegraph in a browser's own Performance panel before.
How Do You Ask Why a Component Re-Rendered?
Clicking a component in the flamegraph shows a right-hand panel with "Why did this render?" (enable it via the gear icon settings if it's not showing). It tells you one of:
- Props changed, and lists which ones
- State changed
- Hooks changed
- Parent component re-rendered
This is the step that turns a hunch into a fact. I've profiled components I was certain were memoization candidates, only to find the actual re-render trigger was a context value changing three levels up, memoizing the leaf component would have done nothing.
// The classic cause: a new object literal on every render
function ProductList({ products }) {
return (
<FilterPanel
// New object every render, breaks React.memo on FilterPanel
options={{ sortBy: 'price', direction: 'asc' }}
/>
);
}
// Fixed: stable reference across renders
const DEFAULT_OPTIONS = { sortBy: 'price', direction: 'asc' };
function ProductList({ products }) {
return <FilterPanel options={DEFAULT_OPTIONS} />;
}
{ sortBy: 'price', direction: 'asc' } is a new object on every single render of ProductList, so any React.memo on FilterPanel sees "new props" every time and re-renders anyway, the memoization does nothing because the actual cause was never fixed.
How Do You Fix the Actual Cause and Re-Profile?
Once you know why a component re-rendered, the fix is usually one of three things:
| Cause found in Profiler | Fix |
|---|---|
| New object/array literal passed as a prop | Move it to a module-level constant, or wrap with useMemo |
| New function passed as a prop | Wrap with useCallback, or move the function outside the component if it doesn't need closure over props/state |
| Parent re-renders and this child has no memo | Add React.memo, but only after confirming its props are actually stable |
| Context value changes trigger unrelated consumers | Split the context into smaller, more specific contexts |
Re-record after each fix. It's tempting to apply all four fixes at once and move on, but re-profiling after each one tells you which fix actually mattered, which matters when you're explaining the change in a PR review six months later and can't remember why a useMemo is there.
- Apply exactly one fix from the table above.
- Re-record the same interaction and compare the new flamegraph to the previous one.
- Only move to the next fix once the current commit shows the component skipped (gray) or its render duration dropped meaningfully.
How Do You Interpret Ranked Commits?
The Profiler's "Ranked" view (a toggle next to the flamegraph) sorts components by render duration within a commit instead of showing the tree shape. This is the fastest way to find your single biggest offender in a deeply nested tree, skip the flamegraph's tree-shaped view entirely and go straight to Ranked when you just want to know "what's slow," not "why is the tree shaped this way."
When Isn't the Profiler the Right Tool?
The Profiler measures React's own render and commit work. It won't catch a slow synchronous computation inside an event handler that never reaches React's render phase, use the Chrome Performance panel's "Interactions" track for that, or a console.time/console.timeEnd pair around the suspect function. If your INP is failing but the Profiler shows fast renders, the bottleneck is probably in your own JavaScript, not React's reconciliation.
Interaction to Next Paint is a Core Web Vital, abbreviated INP, that measures the delay between a user interaction and the browser's next visual update, replacing the older First Input Delay metric as of March 2024. A React app can have a perfectly fast Profiler flamegraph and still fail INP if the slow work happens outside React's render cycle entirely.
Conclusion
The Profiler replaces "this component feels slow" with "this component re-rendered because prop options changed, and here's the object literal causing it." That specificity is what separates a targeted useMemo from a defensive React.memo sprinkled across a codebase without evidence it helped anything.