Skip to content

Profiling Unnecessary Re-Renders With the React Profiler

A hands-on workflow for using the React DevTools Profiler to find and fix unnecessary re-renders, from recording a session to reading the flamegraph.

· · 8 min read
A data dashboard graphical user interface

Quick Take

I used to guess which component was slow and add React.memo everywhere as insurance. That's backwards. The Profiler tells you exactly which component re-rendered and why, before you touch a single line of code.

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.

Performance analytics graphs displayed on a laptop screen
Photo by Luke Chesser on Unsplash

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.memo blocked 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.

Close-up of a laptop screen displaying colorful lines of source code
Photo by Caspar Camille Rubin on Unsplash
// 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 ProfilerFix
New object/array literal passed as a propMove it to a module-level constant, or wrap with useMemo
New function passed as a propWrap 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 memoAdd React.memo, but only after confirming its props are actually stable
Context value changes trigger unrelated consumersSplit 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.

  1. Apply exactly one fix from the table above.
  2. Re-record the same interaction and compare the new flamegraph to the previous one.
  3. Only move to the next fix once the current commit shows the component skipped (gray) or its render duration dropped meaningfully.
Silver analog stopwatch lying face-up, symbolizing precise timing measurement
Photo by Stanislav on Unsplash

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.

Frequently Asked Questions

How do I open the React DevTools Profiler?
Install the React Developer Tools browser extension, open your app in a tab, open DevTools, and click the 'Profiler' tab next to 'Components'. Click the record button, perform the interaction you want to measure in your app, click stop, and the Profiler renders a flamegraph of every component that re-rendered during that window, with timing for each.
What does a gray bar mean in the Profiler flamegraph?
A gray bar means that component did not re-render during the recorded commit, React skipped it entirely. Colored bars (yellow through red, scaled by render duration) mean the component did re-render. If a component you expected to be skipped shows a colored bar, that's your signal to check why, usually a new object or function reference being passed as a prop on every parent render.
Does React.memo always fix a re-render found in the Profiler?
No, and adding it blindly can make things worse by adding a comparison cost to every render without preventing any. React.memo only helps when the component's props are actually stable between renders. If a parent passes a new inline object or arrow function as a prop every render, React.memo's shallow comparison always sees new props and re-renders anyway, wrap the object with useMemo or the function with useCallback first, or fix it at the source.