Skip to content

Profiling React Renders With the DevTools Profiler

Finding 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.

· · 5 min read

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.

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

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

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.

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.