open atlas
↑ Back to track
React, zero to senior RCT · 05 · 01

Profiler and measurement: find the 600 ms commit before touching React.memo

Profile before optimizing: the React DevTools flamegraph shows which commits cost what, the Profiler API turns that into numbers you can ship, and INP — not the 16 ms frame myth — is the budget users actually feel. A team that memoized for two sprints fixed 4 ms and missed 620.

RCT Middle ◷ 17 min
Level
FoundationsJuniorMiddleSenior
Already know this unit? Take a 1-minute quick check →

The dashboard team spent two sprints on a memoization campaign: forty components wrapped in React.memo, every callback in useCallback, a hand-written comparison function for the chart props. The PR description bragged that re-renders were down 60%, with a demo gif to prove it. Then the on-call channel lit up anyway — support kept forwarding the same complaint: “the filters freeze”. Field monitoring put INP at 750 ms on the filter dropdown, exactly where it had been before the campaign. One hour with the React DevTools Profiler told the real story. The wasted re-renders the team had eliminated cost about 4 ms per commit. The freeze was a single 620 ms commit in which changing the date range forced a 9,000-point SVG chart to remount, because a parent passed it a fresh key on every filter change. Two sprints had optimized noise; the signal was one flamegraph bar wide enough to read from across the room. Nobody had profiled before optimizing.

Reading the flamegraph: commits, not components

The React DevTools Profiler records a session of interactions and slices it into commits — every time React actually flushed changes to the DOM, you get one bar in the timeline. Selecting a commit shows a flamegraph: each row of the tree is a component, the width of its bar is how long it (plus its children) took to render in that commit, and gray bars are components that did not render at all. This framing matters: the question is never “which component is slow” in the abstract, it is “which commit is slow, and what inside it burned the time”. A 600 ms commit with one wide bar is a different disease than sixty 10 ms commits triggered by a chatty subscription — and they need opposite cures.

Two settings turn the tool from a chart into a diagnosis. “Record why each component rendered” annotates every bar with the cause: props changed (and which ones), state changed, a hook changed, or the parent rendered. That single annotation kills most guesswork — the chart remount in the Hook story showed up as “key changed”, which no amount of staring at component code would reveal. Second, the “Highlight updates” checkbox paints rectangles over everything that re-renders live, which is the fastest way to spot a context provider rerendering half the page on every keystroke. Profile in development first for the rich data, but remember development React is materially slower — confirm any number that drives a decision against a production profiling build (react-dom/profiling), which keeps timing hooks at a few percent overhead.

The Profiler API: numbers you can ship

DevTools is interactive; the <Profiler> component is programmable. Wrap a subtree, and React calls onRender after every commit inside it with timing data you can log, aggregate, or send to your monitoring backend:

import { Profiler } from "react";

function onRender(id, phase, actualDuration, baseDuration, startTime, commitTime) {
  // actualDuration: time spent rendering this commit (with memoization applied)
  // baseDuration: estimated worst case — every descendant rendering, no memo
  metrics.send("react-commit", {
    id,                       // "FilterPanel"
    phase,                    // "mount" | "update" | "nested-update"
    actual: actualDuration,   // e.g. 4.2 (ms)
    base: baseDuration,       // e.g. 38.7 (ms)
  });
}

<Profiler id="FilterPanel" onRender={onRender}>
  <FilterPanel />
</Profiler>

The pair worth internalizing is actualDuration versus baseDuration. actualDuration is what this commit really cost; baseDuration is the sum of each descendant’s most recent render time — an estimate of the worst case where nothing is memoized. When actualDuration sits far below baseDuration, your memoization is earning its keep; when the two track each other, the React.memo wrappers are decoration. That comparison is exactly the evidence the Hook team never collected: their memo campaign moved actualDuration from 42 ms to 38 ms on the commit that mattered — within noise — because the 620 ms remount bypassed memoization entirely. Tradeoff: <Profiler> is cheap but not free (React does extra bookkeeping per commit inside profiled trees), so in production wrap the two or three interactions you actually monitor, not the root.

Quiz

In onRender data for a commit, actualDuration is 39 ms and baseDuration is 41 ms, even though the subtree is wrapped in React.memo extensively. What does this tell you?

INP and the 16 ms myth

The number everyone quotes — 16.7 ms, one frame at 60 Hz — is a budget for continuous motion: animation, scrolling, dragging. Miss it during a drag and the user sees stutter. But a click is not an animation. For discrete interactions the user-facing metric is INP (Interaction to Next Paint): from the keypress or click until the next frame paints, including input delay (the main thread was busy when the event arrived), processing (your handlers, React’s render and commit), and presentation delay (style, layout, paint). The thresholds are 200 ms for “good” and 500 ms for “poor” — roughly 12 to 30 frame-budgets, and your page’s INP is approximately its worst such interaction over the visit, not the average.

This is why the 16 ms framing misleads optimization work in both directions. It makes a 30 ms commit on a button click look like a violation worth a refactor, when the user cannot distinguish 30 ms from instant. And it says nothing about the real killer: any main-thread task over 50 ms is a long task that adds input delay to whatever the user does next — a 620 ms commit doesn’t just delay its own interaction, it freezes every click that arrives during it. The operational loop, in order: read INP from field data (it is interaction-specific, so lab numbers on a fast laptop routinely miss it), find the worst interaction, profile that interaction, fix the widest bar, measure again. The Hook team ran this loop backwards — fix first, measure never.

Quiz

A click handler triggers a React commit measured at 35 ms. A teammate flags it: 'that is more than two frames, we must memoize'. What is the correct call?

Recall before you leave
  1. 01
    Your monitoring shows INP of 700 ms on a dropdown. Describe the full diagnostic loop you would run before changing any code, naming the tools and the numbers you would read.
  2. 02
    Explain what actualDuration and baseDuration mean in the Profiler onRender callback, and how comparing them tells you whether a memoization campaign was worth it.
Recap

React performance work has a non-negotiable first step: measure, because intuition reliably points at the wrong subtree. The React DevTools Profiler records interactions as a series of commits; each commit’s flamegraph shows render cost as bar width, gray bars for components that bailed out, and — with “record why each component rendered” on — the reason each bar exists: changed props, changed state, a parent render, or a changed key (the silent remount trigger that defeated the Hook team’s two-sprint memo campaign while a 620 ms chart remount hid in plain sight). The programmable counterpart is the Profiler component: its onRender callback reports actualDuration (what the commit cost with memoization applied) against baseDuration (the estimated no-memoization worst case), and the gap between them is the only honest measure of whether React.memo is earning anything — wrap the few interactions you monitor, not the root, since profiling has a small per-commit cost. Judge impact in the user’s units, not React’s: discrete interactions are governed by INP — input delay, processing, presentation delay, good under 200 ms, poor over 500 ms, dominated by the worst interaction of the visit — while the famous 16.7 ms budget applies to continuous motion like scrolling and animation. A 35 ms click commit needs nothing; a 620 ms one is a long task that also freezes every other click that lands during it. The loop is: field INP names the interaction, the flamegraph names the commit, fix the widest bar, measure again. Now when you see a teammate reach for React.memo before opening the Profiler, you know exactly what to ask: which commit, which bar, and what does field INP say about this interaction?

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 6 done
Connected lessons

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

Apply this

Put this lesson to work on a real build.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.