Skip to content
React rc performance 5 min read

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.

ViewWhat it showsBest for
FlamegraphThe component tree for one commit, widths scaled to render timeSeeing which subtree dominated a commit
RankedA flat list of components sorted by render durationQuickly spotting the single slowest component
TimelineScheduling, suspense, and events over the whole sessionUnderstanding 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.

ArgumentMeaning
idThe id prop you passed, identifying which tree committed
phase"mount", "update", or "nested-update"
actualDurationTime spent rendering this commit (ms) — lower after memoization
baseDurationEstimated time to render the whole subtree without memoization
startTimeWhen React began rendering this commit
commitTimeWhen 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 actualDuration against baseDuration to 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.
Last updated June 14, 2026
Was this helpful?