Profiling with the Profiler
Performance problems are invisible until you measure them. React gives you two powerful tools for this: the React DevTools Profiler tab for interactive investigation, and the <Profiler> API component for programmatic measurement in production-like scenarios.
React DevTools Profiler
Install the React Developer Tools browser extension (Chrome or Firefox). Open DevTools and you will see a new Components tab and a Profiler tab. The Profiler is the performance investigation tool.
Open your app in development mode (
npm run dev)Open browser DevTools → React tab → Profiler
Click the circular Record button (turns red)
Interact with the part of your app you want to investigate — click buttons, type in forms, navigate routes
Click the Record button again to stop recording
Inspect the results in the flamegraph or ranked chart views
Reading the Flamegraph
The flamegraph view shows every render that occurred during the recording. Each commit (a batch of DOM updates) is shown as a separate bar at the top. Select a commit to see the component tree for that commit.
Bar width = time the component took to render. Wider = slower.
Color = relative render time. Yellow/orange = slower, green = fast, gray = did not render in this commit.
Gray bars = the component was not re-rendered in this commit — React reused its previous output.
Clicking a bar shows the component name, render duration, and which props/state/hooks changed.
The Ranked Chart
Switch to the Ranked tab to see components ordered by render time — the slowest at the top. This is the most actionable view: it tells you exactly where to focus your optimisation effort without having to scan the entire tree.
A component appearing near the top of the ranked chart on every commit is a prime candidate for React.memo or useMemo. A component appearing at the top only occasionally means the slow path is triggered by a specific interaction.
Why Did This Component Render?
Enable "Record why each component rendered while profiling" in Profiler settings (the gear icon). After recording, clicking any component bar shows a "Why did this render?" panel explaining the cause:
"Props changed" — lists exactly which props changed and their old vs new values
"State changed" — which useState/useReducer value changed
"Context changed" — a context provider above this component updated
"The parent component rendered" — no props/state/context changed; this is a cascading render from the parent
"First render" — component mounted for the first time
The most actionable finding is "The parent component rendered." That means this component re-rendered for no reason of its own — it is a candidate for React.memo.
The Profiler API Component
For programmatic measurement (e.g., logging to analytics, CI benchmarks), use the <Profiler> component from React:
import { Profiler } from 'react'
function onRenderCallback(
id, // the "id" prop of the Profiler tree that just committed
phase, // "mount" | "update" | "nested-update"
actualDuration, // time spent rendering the committed update (ms)
baseDuration, // estimated time to render the entire subtree without memoization (ms)
startTime, // when React began rendering this update
commitTime, // when React committed this update
) {
console.log(`[${id}] ${phase} — ${actualDuration.toFixed(2)}ms`)
// In production, send this to your analytics service
}
function App() {
return (
<Profiler id="Navigation" onRender={onRenderCallback}>
<Navigation />
</Profiler>
)
}Interpreting Profiler Callback Values
actualDuration— the real time spent. High values mean slow renders. Should decrease as you add memoization.baseDuration— estimated time if there were no memoization. Useful as a baseline to compare againstactualDuration.If
actualDurationis much lower thanbaseDuration, memoization is working effectively.If
actualDuration≈baseDuration, memoization is not helping (memo is being bypassed).
// Wrapping multiple sections to compare them
function App() {
return (
<>
<Profiler id="Sidebar" onRender={onRenderCallback}>
<Sidebar />
</Profiler>
<Profiler id="MainContent" onRender={onRenderCallback}>
<MainContent />
</Profiler>
</>
)
}
// Sample output in console:
// [Sidebar] update — 0.40ms
// [MainContent] update — 47.82ms ← investigate this oneA Practical Profiling Workflow
Identify the user interaction that feels slow (e.g., typing in a search box).
Open React DevTools Profiler, enable "record why each component rendered".
Record while performing the slow interaction 5–10 times.
Stop recording and open the Ranked chart — note the top 3 slowest components.
For each: click into it. Is it "parent rendered"? → React.memo candidate. Slow own logic? → useMemo candidate.
Apply one optimisation at a time. Re-profile to verify the change helped.
Repeat until the interaction feels smooth.
Common Bottlenecks Found via Profiling
Large list re-renders — every item in a 500-item list re-renders on filter. Fix: React.memo on list items + stable key.
Context re-rendering everything — a ThemeContext or AuthContext update triggers 200 consumers. Fix: split contexts, memoize values.
Expensive inline computations — filtering/sorting a large array on every render. Fix: useMemo.
Unstable child components — a child defined inside a parent render function remounts every time. Fix: move definition outside.
Cascading parent renders — an App-level state change (modal open) re-renders the entire tree. Fix: colocate state, React.memo on stable subtrees.