Command Palette

Search for a command to run...

Hectal
PHASE 16Advanced ~21 min· topic 6 of 10

Topic 16.6

Measuring Performance: Profiler and DevTools

In one line

Measure before optimising: the React DevTools Profiler records every commit and shows which components rendered, why, and for how long; the <Profiler> component reports the same data to your code; and the browser's Performance panel shows the whole picture, including layout, paint and long tasks.

Think of it like this

A doctor doesn't give medicine because you "feel slow". They take your temperature, check your pulse, maybe order a blood test, and only then decide. Optimising code without measuring is like taking random medicine: it may do nothing, or make things worse. Measure, change one thing, measure again.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Profiler (DevTools)
A tab in React DevTools that records commits and shows which components rendered and how long they took.
Commit
One time React applied changes to the DOM. The profiler shows one bar per commit.
Flame graph
A chart of the component tree where each bar's width shows how long that component took.
<Profiler>
A React component that reports render timings for its subtree to an onRender callback.
actualDuration
Milliseconds spent rendering the profiled subtree in this commit.
baseDuration
Estimated milliseconds to render the whole subtree with no memoisation.
Long task
Any piece of main-thread work over 50 ms. While it runs, the page can't respond to input.
INP
Interaction to Next Paint: how quickly the page visibly responds to a click or key press. Under 200 ms is good.

Step by step

01Reproduce the slow interaction

Pick one concrete action that feels slow: "typing in the search box lags". Build for production, open the page with CPU throttling, and confirm you can feel it.

terminalwhole filebash
npm run build
npm run preview
#  ➜  Local:   http://localhost:4173/

02Record in the React Profiler

Open DevTools, go to the Profiler tab, click record, do the slow action once, stop. Step through the commits. In the flame graph, look for wide coloured bars: components that took long and rendered when they didn't need to.

03Ask why it rendered

With "record why each component rendered" on, click the slow component. If it says "The parent component rendered" and nothing changed, it's a candidate for moving state down or memo. If it says "Props changed: (onSelect)", that prop is unstable.

04Check the whole frame in the Performance panel

Record the same action in the Performance tab. If the long task is mostly React rendering, fix rendering. If it's layout or paint (thousands of DOM nodes), fix the DOM size (Topic 16.9). If it's your own code (parsing, sorting), cache or move it.

Check the whole frame in the Performance paneldiagram
Rendering diagram…

05Change one thing, measure again

Apply one fix, record again, compare commit durations. Keep the fix only if the numbers improved. Remove optimisations that didn't help: they cost complexity.

Try it yourself

  1. 1

    Profile a deeper tree

    In the first example, wrap the whole <main> in a third <Profiler id="App">. Predict how many lines each click logs now, then run. Nested profilers each report their own subtree.

  2. 2

    Remove the nested update

    In the tooltip example, delete the state and the layout effect and compute const width = text.length * 8; during render. Predict the log: one commit per change, phase mount then update.

  3. 3

    Use the real DevTools

    Install React DevTools in your browser, open any React app you've built locally, and record one click in the Profiler with "record why each component rendered" turned on. Find one component that rendered only because its parent did.

Code & diagrams

The <Profiler> component reports each commit New tab

Liking only commits inside Feed, so only the Feed profiler reports. Toggling dark mode renders App, which renders both subtrees. Add `console.log(actualDuration)` to see timings: they vary on every run, which is why they aren't in the expected output.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

[Profiler] Sidebar mount
[Profiler] Feed mount
--- like (state inside Feed)
[Profiler] Feed update
--- toggle dark (state in App)
[Profiler] Sidebar update
[Profiler] Feed update
Spotting a nested update New tab

Every change costs two renders: the normal one and a nested update from the layout effect. That's a measurement worth having: here the width could be computed during render instead (Topic 6.2), saving a render each time.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

After mount: Tooltip:mount, Tooltip:nested-update
After one click: Tooltip:update, Tooltip:nested-update
Sending profiler data somewhere usefuljsx
function onRender(id, phase, actualDuration, baseDuration) {
  // Only report slow commits, and batch them.
  if (actualDuration > 16) {
    queue.push({ id, phase, actualDuration: Math.round(actualDuration), baseDuration: Math.round(baseDuration) });
  }
}

// Needs a profiling build in production (see the note above),
// otherwise onRender is never called there.
<Profiler id="Checkout" onRender={onRender}>
  <Checkout />
</Profiler>

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Trust timings from the development build

Measure a list's render time with console.time in npm run dev with StrictMode on, then optimise based on that number.

terminal
$ Console
── what you'll see ──
render list: 48 ms (development, StrictMode: rendered twice)
render list: 9 ms (production build)

Myth vs fact

Myth

If DevTools shows lots of renders, the app is slow.

Fact

Render count alone means little. What matters is how long commits take and whether the user notices. Many fast renders are fine.

Myth

<Profiler> works in every build.

Fact

It's disabled in the standard production build. Use the profiling build to measure production.

Myth

React DevTools tells you everything about performance.

Fact

It only sees React's rendering. Layout, paint, network and your own code show up only in the browser's Performance panel.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    actualDuration vs baseDuration is the quickest memoisation audit: if actualDuration is close to baseDuration on updates, nothing is being skipped; if it's much lower, memoisation (yours or the compiler's) is working.

  • ▸

    React 19.2's Performance Tracks label each piece of React work with its priority lane, so you can see a transition render being interrupted by a click directly in Chrome's timeline, next to network and layout.

  • ▸

    For field data (real users), measure INP with the web-vitals library or the browser's Event Timing API. Lab profiles tell you why; field data tells you whether users actually suffer.

  • ▸

    Use performance.mark() and performance.measure() around your own expensive code: the entries appear in the Performance panel's Timings track.

Remember this

  1. 1

    Measure in production mode. The development build of React (what npm run dev and Hectal's live preview use) is much slower than production: it runs extra checks, keeps stack traces, and in StrictMode renders everything twice. Real timings come from a production build (npm run build then npm run preview in Vite, next build then next start in Next.js). Test on a slow device too: the browser's DevTools can throttle the CPU (4× or 6× slowdown) to imitate a cheap phone.

  2. 2

    React DevTools Profiler. React DevTools is a browser extension (Chrome, Firefox, Edge). Its Profiler tab records while you use the app. It shows one bar per commit (one time React updated the DOM). For each commit, a flame graph shows the component tree: grey components didn't render, coloured ones did, and wider/yellower means slower. The ranked view lists the slowest components first. Clicking a component shows how long it took in each commit.

  3. 3

    "Why did this render?" In the Profiler settings, turn on Record why each component rendered while profiling. Then each component shows a reason: "The parent component rendered", "Props changed: (onSelect)", "Hook 2 changed", "Context changed". That's how you find the unstable prop that breaks a memo (Topic 16.3). The Components tab also has Highlight updates when components render, which flashes a border around every component that renders as you use the app.

  4. 4

    **The <Profiler> component.** import { Profiler } from "react". Wrap a subtree: <Profiler id="Feed" onRender={onRender}>...</Profiler>. After every commit that touches the subtree, React calls onRender(id, phase, actualDuration, baseDuration, startTime, commitTime). phase is "mount", "update" or "nested-update" (an update caused during the commit itself, for example by setState in a layout effect). actualDuration is the time spent rendering the subtree in this commit; baseDuration estimates a full re-render with no memoisation, so comparing them tells you how much memoisation saves. You can nest profilers and send the numbers to your analytics.

  5. 5

    Profiling in production. <Profiler> and DevTools profiling are switched off in the normal production build to keep it fast. To profile a production build you use React's profiling build (alias react-dom/client to react-dom/profiling in your bundler; Next.js has a next build --profile flag). Use it in a special build, not for all users.

  6. 6

    The browser Performance panel. React only sees rendering. The browser's Performance tab records everything: JavaScript, style calculation, layout, paint, garbage collection, network. Look for long tasks (any task over 50 ms, marked with red corners), which block input. React 19.2 adds React Performance Tracks to Chrome's Performance panel (in development and profiling builds): a Scheduler track showing which priority React is working on (blocking, transition, idle) and a Components track showing render and effect times. Interaction to Next Paint (INP), a Core Web Vital, measures how long the page takes to respond to clicks and typing; aim for under 200 ms.

Explain it without notes

01

Why should you measure in a production build?

02

What does the React DevTools Profiler show, and how does "why did this render" help?

03

Explain the arguments of a <Profiler>'s onRender callback.

04

When would you use the browser's Performance panel instead of React DevTools?

Practice

01

Wrap a ProductGrid in a <Profiler> and log a warning whenever an update takes longer than one frame (16 ms).

02

A memoised Row still shows in the DevTools flame graph on every keystroke, with the reason "Props changed: (style)". What's the likely cause and fix?

Trade-offs

  • ↔

    Measuring takes time up front, but saves you from optimisations that add complexity without helping users.

  • ↔

    Profiling builds and <Profiler> add a small overhead, so use them in special builds or sampled sessions, not for every user.

Done when you can

  • I can record and read a React DevTools Profiler session.

  • I can find why a component rendered.

  • I can use <Profiler> and explain its onRender arguments.

  • I know why timings must come from a production build.

  • I can use the browser Performance panel to find long tasks.