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.
TL;DR: 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.
Step 1: 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.
Step 2: 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.
Step 3: Ask Why It 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.
Step 4: Fix the Actual Cause, Then 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.
Interpreting 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 the Profiler Isn't 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.
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.