skip to content

In React, what does wrapping part of your tree in `<Profiler id="Sidebar" onRender={handleRender}>` actually do — what does it render, and when does React call the onRender callback?

level: juniorimportance: should knowfreq 35%

answer

  1. a component that measures, not renders
  2. two props only
  3. fires after commits, not on events
  4. callback gets id, phase, and durations
  5. transparent wrapper, no DOM node emitted

basics

~20 s

React's Profiler component renders its children unchanged and emits no DOM of its own. Its only job is to call your onRender callback after every commit in which that subtree rendered, passing the id you gave it plus timing numbers for that commit.

solid answer

~50 s

`Profiler` is a built-in component imported from `react`. It takes two props: `id`, a string label for the part of the UI you are measuring, and `onRender`, a callback React invokes after each commit that included a render of the profiled subtree. It is transparent: it renders exactly its children, adds no wrapper element, and does not cause extra renders or change how reconciliation treats the subtree. The callback receives `(id, phase, actualDuration, baseDuration, startTime, commitTime)`, so it is the programmatic route to render timings — you can log them, aggregate them, or ship them somewhere, rather than watching a devtools session. You can use several Profilers and nest them; each one reports for its own subtree, and profilers in the same commit share a commit timestamp. Because the instrumentation costs something, React disables it in the standard production build.

code

jsx · 17 lines
jsx
import { Profiler } from 'react';

function handleRender(id, phase, actualDuration, baseDuration, startTime, commitTime) {
  console.log(id, phase, actualDuration.toFixed(2), baseDuration.toFixed(2), startTime, commitTime);
}

export default function Page({ items }) {
  return (
    <Profiler id="Sidebar" onRender={handleRender}>
      <ul>
        {items.map((item) => (
          <li key={item.id}>{item.label}</li>
        ))}
      </ul>
    </Profiler>
  );
}

go deeper

for a junior

Know the two props — id and onRender — and be able to say that Profiler renders only its children and calls your callback after commits, not on events.

for a middle

Be ready to recite the callback arguments in order and explain what a commit is, why the callback runs after the work rather than before, and why nesting profilers produces several calls with one shared commit timestamp.

for a senior

Show that you know measurement is not free: explain why profiling is off in the standard production build and why development timings, inflated by StrictMode's double render, are only useful as before/after comparisons.

for a principal

Own the question of what the instrumentation is for. Argue when a permanent Profiler earns its place — a regression check on a known-hot subtree — versus when a one-off devtools session answers the question at zero shipped cost.

## What the component is `Profiler` is a built-in React component, imported from the `react` package alongside things like `Fragment` and `StrictMode`: ```jsx import { Profiler } from 'react'; <Profiler id="Sidebar" onRender={handleRender}> <Sidebar /> </Profiler> ``` It takes two props. `id` is a string you choose; it is echoed back to your callback so you can tell which measurement came from which part of the UI. `onRender` is a function React calls with the timings. ## What it renders Nothing of its own. `Profiler` renders exactly its children — no wrapper `div`, no `span`, no extra DOM node, no extra text node. If the subtree renders a table row, you can wrap it in a Profiler without breaking the table's HTML structure, which is not true of a wrapper `div`. It also does not introduce a new place where state lives, does not reset children when it re-renders, and does not make its children re-render more often than they otherwise would. Conceptually it is an observer attached to a slice of the tree, not a layer in the tree. ## When the callback fires React calls `onRender` **after a commit**, once per commit in which components inside the profiled subtree rendered. "Commit" is the moment React applies a finished render to the DOM; the callback fires after that work for the subtree is done, so the numbers describe work that has already happened. It is not an event listener: user interactions that cause no render produce no call, and a render elsewhere in the app that does not touch this subtree produces no call either. The first call for a mounted Profiler describes the initial mount; later ones describe updates. The callback signature is: ```jsx function handleRender(id, phase, actualDuration, baseDuration, startTime, commitTime) { // id: the string you passed to <Profiler id="..."> // phase: 'mount' | 'update' | 'nested-update' // actualDuration: ms React spent rendering this subtree for this update // baseDuration: estimated ms to render the whole subtree with no memoization // startTime: when React began rendering this update // commitTime: when React committed this update } ``` `phase` tells you whether this was the subtree's first render, a normal update, or an update scheduled from inside a lifecycle/effect during a previous commit. The two durations are the interesting pair and are worth studying separately. `startTime` and `commitTime` are timestamps on the same clock React uses internally, which is `performance.now()` where the environment provides it. ## Several profilers, and nesting You can mount as many Profilers as you like, and you can nest them: ```jsx <Profiler id="page" onRender={handleRender}> <Header /> <Profiler id="list" onRender={handleRender}> <ItemList items={items} /> </Profiler> </Profiler> ``` If something inside `ItemList` re-renders, both callbacks fire — the inner one measuring the list, the outer one measuring the page subtree that contains it. Both calls carry the same `commitTime`, because they describe the same commit, which is exactly how you fold several measurements back into one "this commit cost X" record. Ids do not need to be unique from React's point of view, but reusing an id makes your own aggregation ambiguous, so treat them as stable names. ## What it costs, and where it works Measuring is not free: React has to time each unit of work in the profiled subtree and keep the bookkeeping. That overhead is the reason profiling is disabled in the standard production build — a Profiler you leave in shipped code does not silently slow every user down with a full timing pass, but it also does not hand you real production numbers without a profiling-enabled build. During development the picture is distorted in the other direction: development builds are slower than production, and StrictMode deliberately renders components twice, so absolute millisecond values read high. Use the numbers for comparison — before versus after a change, one subtree versus another — rather than as an absolute budget. ## Where it fits The Profiler component is the programmatic sibling of the interactive profiler in React DevTools: same underlying timing data, but delivered to your code instead of a UI, which is what makes automated regression checks and telemetry possible.

  • Does adding a Profiler around a subtree change how often that subtree renders?
    No. It renders its children directly and takes part in reconciliation as a pass-through, so the same components re-render for the same reasons as before. What changes is that React does extra timing bookkeeping for the wrapped work, which makes those renders slightly more expensive to perform — the measurement has a cost, but it does not add renders.
  • Two nested Profilers both fire for one update. How do you tell that the two records describe the same commit?
    By `commitTime`. React passes the same commit timestamp to every Profiler callback that fires for a given commit, so it works as a natural grouping key. `startTime` is also shared for the same update, while `id` and the durations differ per subtree. Group on `commitTime` when you want a per-commit total instead of per-subtree rows.
  • Why are the millisecond values you see in `npm run dev` not trustworthy as absolute numbers?
    Development builds include warnings, checks and unminified code that production strips, so everything is slower. On top of that, StrictMode intentionally double-renders components in development. Read development timings as relative signals — did this change make the subtree cheaper? — not as a budget you can promise in production.

saying these in an interview costs you the question

  • Thinks Profiler renders a wrapper div around its children
  • Believes onRender fires on every user event
  • Assumes Profiler works normally in a standard production build
  • Says wrapping a subtree in Profiler makes it re-render more
  • Treats development-build milliseconds as real production timings

context