Skip to main content

Measure Transition Performance in React

Marking updates as transitions is only half the battle. To truly optimize your app, you must measure whether your transitions are actually helping. React DevTools Profiler is the primary tool for understanding render times, identifying components that re-render unnecessarily, and confirming that your transitions are having the intended effect. In this article, you'll learn to profile concurrent rendering, interpret the data, and spot performance bottlenecks.

Profiling is not optional. Without it, you're guessing. With it, you'll see exactly where your app spends time and optimize with confidence.

Opening the React DevTools Profiler

The React DevTools Profiler is a browser extension. Install it, then:

  1. Open your React app in the browser.
  2. Open DevTools (F12 or Ctrl+Shift+I).
  3. Click the "Profiler" tab (you may need to scroll through the tabs to find it).
  4. Click the red circle (record button) to start profiling.
  5. Interact with your component (type in a search, click a button, etc.).
  6. Click the red circle again to stop recording.

React will show you a detailed breakdown of every render that happened during your interaction.

Reading a Profiler Trace

Here's what a profiler trace shows for a simple search component:

Render: SearchResults (14 ms)
└─ Filter Computation (12 ms)
└─ List Re-render (2 ms)
Commit: (1 ms)

The numbers tell the story:

  • Render: The time React spent preparing the update (checking which components changed, calling render functions).
  • Commit: The time React spent applying changes to the DOM.
  • Individual component times: How long each component's render function took.

A transition render often appears as two separate renders in the trace:

  1. High-priority render (fast, e.g., 0.5 ms) — updates the input field.
  2. Transition render (slow, e.g., 150 ms) — filters the list.

If you see only one slow render, your transition isn't working correctly.

Let's profile the filterable search component from earlier:

import { useState, useTransition, useMemo } from 'react';

export function ProfiledFilterSearch() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();

const allProducts = useMemo(() => generateProducts(100000), []);

const filtered = useMemo(() => {
return allProducts.filter(p =>
p.name.toLowerCase().includes(query.toLowerCase())
);
}, [allProducts, query]);

const handleChange = (e) => {
const newQuery = e.target.value;
setQuery(newQuery);

// In a real profiling session, this startTransition wrapper
// would not exist. Remove it to see the difference.
startTransition(() => {});
};

return (
<div>
<input
value={query}
onChange={handleChange}
placeholder="Type to search..."
/>
{isPending && <p>Filtering...</p>}
<ul>
{filtered.slice(0, 20).map(p => (
<li key={p.id}>{p.name}</li>
))}
</ul>
</div>
);
}

function generateProducts(count) {
return Array.from({ length: count }, (_, i) => ({
id: i,
name: `Product ${i}`,
}));
}

Now profile it:

  1. Open DevTools Profiler.
  2. Click record.
  3. Type "hello" in the input field slowly, one character at a time.
  4. Stop recording.

Without useTransition, the profiler shows:

  • Every keystroke triggers a render taking 50–100 ms.
  • The input lags noticeably because React is blocked computing the filter.

With useTransition (correctly implemented), you'd see:

  • Input keystroke → fast render (1–5 ms).
  • Filter computation → separate render starting a moment later (50–100 ms).
  • If you type again before the filter finishes, only the new filter render starts; the old one is abandoned.

Flame Chart Deep Dive

The Profiler shows a flame chart. Wider bars = longer time. Taller stacks = more components. Here's what to look for:

ComponentA ████████ (40 ms)
└─ ComponentB ██████ (35 ms)
│ └─ ComponentC ██ (2 ms)
│ └─ ComponentD ████ (33 ms)
└─ ComponentE (5 ms)

In this example, ComponentD is the bottleneck—it's taking 33 ms to render. If it's rendering unnecessarily, you can optimize it by:

  • Wrapping it in React.memo to prevent re-renders when props haven't changed.
  • Splitting it into smaller components.
  • Using useMemo to avoid recomputing expensive logic.

Detecting Unnecessary Re-Renders

The Profiler shows why each component re-rendered. If a component re-rendered but its props didn't change, that's a wasted render. Example output:

ComponentA rendered because:
└─ Props changed: (no changes)
└─ State changed: query
└─ Hook dependency changed: [query]

No changes means the parent re-rendered but didn't pass new props. Wrap this component in React.memo:

const OptimizedComponentA = React.memo(function ComponentA({ data }) {
return <div>{data}</div>;
});

Measuring Specific Metrics

Use React's Profiler API to measure render times programmatically:

import { Profiler } from 'react';

function onRenderCallback(
id, // Component ID
phase, // "mount" or "update"
actualDuration, // Time for this render
baseDuration, // Time without memoization
startTime, // When React started rendering
commitTime, // When React committed
interactions // Set of interactions that triggered the update
) {
console.log(`${id} (${phase}): ${actualDuration}ms`);
}

export function App() {
return (
<Profiler id="SearchComponent" onRender={onRenderCallback}>
<FilterSearch />
</Profiler>
);
}

Console will log:

SearchComponent (update): 145ms
SearchComponent (update): 3ms

The first is the transition render (slow), the second is the input render (fast).

Comparing Before and After

Always measure before and after optimizing. Here's a structured approach:

  1. Baseline: Profile the component in its current state. Note the render times.
  2. Hypothesize: Guess which component is slow.
  3. Optimize: Apply a fix (add memo, use useMemo, mark as transition).
  4. Re-profile: Measure the same interaction again.
  5. Validate: Did the render time improve? By how much?

Example improvements:

  • Without useTransition on a 100k-item filter: input lags, render takes 150 ms.
  • With useTransition: input is instant, transition render still takes 150 ms but doesn't block the input.

The absolute render time didn't change, but the perceived performance improved dramatically because the input is responsive.

Common Performance Issues and Fixes

ProblemCauseFix
Component re-renders on every keystrokeParent re-renders unnecessarilyWrap in React.memo, or lift state up
Transition render is slow (>200 ms)Expensive computation in renderMove to useMemo, or split into smaller renders
Input lag even with transitionIncorrect startTransition placementEnsure high-priority state update is outside startTransition
Profiler shows many re-rendersHook dependencies change frequentlyReview dependency arrays in useEffect, useMemo, useCallback

Profiling in Production

Development builds are slow. For accurate measurements, profile in a production build:

npm run build
npm run serve

Production builds are much faster, so a render taking 50 ms in development might take 5 ms in production. Always profile in production to get realistic numbers.

Key Takeaways

  • Use React DevTools Profiler to measure render times and identify bottlenecks.
  • A correctly implemented transition shows two renders: a fast high-priority render and a deferred slow render.
  • Look for components that re-render unnecessarily (no prop changes) and wrap them in React.memo.
  • Measure before and after optimizing to confirm improvements.
  • Profile in production builds for realistic performance data.

Frequently Asked Questions

Why does the Profiler show two renders when I type once?

The first render is the high-priority state update (input), the second is the deferred transition (filter). If you see only one slow render, your transition isn't working.

Is a 100 ms render time bad?

Not necessarily. If it's a transition (low-priority) and the UI stays responsive, it's fine. If it's blocking the input, it's bad.

Can I profile in production without slowing down users?

Use conditional profiling: only enable the Profiler component when a URL parameter is set (e.g., ?profiling=true). Production builds won't show the Profiler overhead to regular users.

How do I know if React.memo is helping?

Profile before and after adding React.memo. If the component renders fewer times or faster, it's helping. If not, the prop is probably changing.

Further Reading