A 340ms long task in the Chrome DevTools Performance panel is a symptom, not a diagnosis. The Main track tells you the main thread was busy for 340ms; it does not tell you which React component renders are inside that window, whether they ran on the blocking lane or a transition lane, or whether the cost was real render work or wasted work. This article is a procedure for closing that gap: read the Scheduler track first to classify the work, then the Components track to name the renders, then cross-check with <Profiler> to decide whether the fix is “make the render cheaper” or “move it off the blocking path” — and re-record to prove the number moved.
Two version pins matter before anything else. React Performance tracks are only available in development and profiling builds of React, and the profiling instrumentation that powers them adds overhead, so it is disabled in production builds by default (React docs: React Performance tracks). The 340ms figure in this article is an illustrative example, not a measurement of any real application. Treat every millisecond you quote as something you recorded on your own build.
Why the Main track alone can’t answer the question
The Performance panel represents main thread activity as a flame chart: the x-axis is time, the y-axis is the call stack, and clicking an event shows duration, self duration, a link to the source line, and a stack trace in the Summary tab (Chrome DevTools Performance features reference). That is enough to see that a task ran long. It is not enough to see React’s own view of the work, because React’s render phase, commit phase, and passive effects are not separate browser tasks — they are slices inside the same long task, often inside minified framework code.
The wrong approach at scale is to open the flame chart, find the widest yellow bar, and start memoizing whatever component name appears near it. That fails for two reasons. First, the widest bar is frequently React internals, not your component. Second, even when you land on the right component, you have not learned why it rendered or whether it rendered on the blocking lane — and those two facts determine which fix is correct.
Step 1: Get onto a build that emits React tracks
React Performance tracks appear automatically in traces recorded with the Performance panel of browsers that provide extensibility APIs, but only in development and profiling builds. In a profiling build, only the Scheduler track is enabled by default, and the Components track lists only components inside subtrees wrapped with <Profiler> — unless the React Developer Tools extension is enabled, in which case all components are included in the Components track even if they are not wrapped (React docs).
To use the profiling build, React documents swapping react-dom/client for react-dom/profiling, and recommends aliasing at build time via bundler aliases rather than editing every import (React docs). A webpack-style alias looks like this:
// next.config.js or webpack config — illustrative
config.resolve.alias['react-dom/client'] = 'react-dom/profiling';
Two cautions. The React docs note that a framework may have built-in support for enabling the profiling build; check your framework’s documentation rather than assuming the alias is the supported path. And do not quote absolute timings from the default development build — it is instrumented for warnings and its absolute numbers are not evidence. The profiling build is the appropriate source for numbers you intend to defend.
Step 2: Read the Scheduler track before the Components track
The Scheduler track has four subtracks, one per priority lane: Blocking (synchronous updates, which could have been initiated by user interactions), Transition (non-blocking background work, usually initiated via startTransition), Suspense (work related to Suspense boundaries, such as displaying fallbacks or revealing content), and Idle (the lowest priority work, done when nothing higher is pending) (React docs).
Every render pass also shows its phases on the timeline: Update (what caused the new render pass), Render (React calls the render functions of the updated subtree), Commit (React submits changes to the DOM and runs layout effects such as useLayoutEffect), and Remaining Effects (passive effects, usually after paint — with the documented exception of discrete events like clicks, where this phase can run before paint) (React docs).
This ordering is the whole method. If your 340ms task shows the expensive render on the Blocking subtrack, the fix is either to make that render cheaper or to move it to a lower priority lane. If it already sits on Transition, moving it will not help — you have a genuine render-cost problem and need to reduce the work. Reading the Components track first skips this fork and sends you toward the wrong fix roughly half the time.
Step 3: Name the renders on the Components track
The Components track visualizes component render durations as a flamegraph, where each entry represents the duration of the corresponding component render and all its descendant children — so a wide bar near the root is inclusive time, not that component’s own cost (React docs). The track follows the same color scheme as the Scheduler phases, which is what lets you line a render entry up against the Blocking or Transition bar above it.
Effects appear on this track too, but React only displays effects that ran for 0.05ms or longer or that triggered an update, to keep the track readable (React docs). If you are hunting a long task and the Components track looks empty for a subtree you know rendered, check whether the work was an effect below that threshold rather than a render.
In development builds, clicking a component render entry lets you inspect potential changes in props (React docs). That is the fastest way to spot an unstable inline prop or a context value that changed identity on every parent render.
Step 4: Catch updates scheduled during render
One pattern deserves its own pass through the trace. If an update was scheduled during a render pass, React can discard completed work and start a new pass — a cascading update. In development builds, React can show which component scheduled the new update, and clicking the “Cascading update” entry displays an enhanced stack trace including the name of the method that scheduled it (React docs). A useEffect that sets state on every render is the classic source; the Cascading update entry names it faster than reading the flame chart will.
Step 5: Use <Profiler> to choose between “expensive” and “wasted”
The Components track tells you which renders are inside the long task. It does not, by itself, tell you whether those renders were necessary. That is what <Profiler> is for. It measures rendering performance of a React tree programmatically through an onRender callback that receives id, phase, actualDuration, baseDuration, startTime, and commitTime (React docs: <Profiler>).
The two durations are the fork in the road:
- actualDuration is the milliseconds spent rendering the profiled subtree and its descendants for the current update, and it indicates how well the subtree makes use of memoization (React docs).
- baseDuration estimates how long it would take to re-render the entire subtree without any optimizations, calculated by summing the most recent render durations of each component in the tree (React docs).
If actualDuration sits close to baseDuration on the update that produced your long task, memoization is not skipping much — this is a genuine render-cost problem, and the fix is to reduce the work or move it off the blocking lane. If actualDuration is far below baseDuration, memoization is already doing real work and adding more of it is unlikely to move the number. commitTime is shared between all profilers in a commit, which lets you group measurements from several <Profiler> boundaries that landed in the same commit (React docs).
Two constraints on this tool. Profiling adds overhead and is disabled in the production build by default; opting into production profiling requires a special production build with profiling enabled (React docs). And components wrapped in <Profiler> are marked in the Components track even in profiling builds, which is how you get named entries without the DevTools extension (React docs). Instrument the subtrees you suspect, not every component — each <Profiler> adds CPU and memory overhead.
A worked example, clearly labeled as hypothetical
Suppose a filter input sits above a large table, and typing produces a 340ms long task. The procedure runs like this:
- Record one keystroke on the profiling build with CPU throttling on. Find the 340ms task on the Main track.
- Scheduler track: the table render appears on the Blocking subtrack, in the same frame as the input. That tells you the work is synchronous and on the interaction path.
- Components track: the flamegraph names the table subtree, and the inclusive bar is wide while the leaf rows are individually cheap. The cost is spread across many renders, not concentrated in one slow component.
- <Profiler> around the table:
actualDurationis close tobaseDurationon the keystroke commit. Memoization is not the lever here. - One change: split the update so the input value commits synchronously and the table update runs at a lower priority — for example by deferring the value the table reads. The mechanism is documented in useTransition, which describes marking state updates as Transitions so they are non-blocking and interruptible.
- Re-record. The keystroke should now produce a short Blocking bar and the table render should appear on the Transition subtrack. The render cost did not shrink; it stopped blocking. That distinction is the payoff of reading the Scheduler track first.
No millisecond improvement is asserted here, because none was measured. The claim the procedure supports is structural: the work moved lanes, and the trace shows it. Whether that improves a field metric is a separate measurement you have to take.
What this method does not cover
React’s Performance tracks are an experimental adoption of Chrome’s Performance panel extensibility API, which surfaces client and server component rendering as well as React scheduling data (Chrome for Developers). The extensibility API itself offers two mechanisms: console.timeStamp, designed for minimal runtime overhead and not adding entries to the browser’s internal performance timeline, and the User Timings API (performance.mark / performance.measure), which does add entries and supports richer detail (Chrome DevTools extensibility API). Custom tracks only appear when Show custom tracks is enabled in Capture settings — check that before concluding the tracks are missing.
Server Components and Server Requests tracks are development-only, and Server tracks are not available in profiling builds (React docs). If your long task is on the client but your data comes from a server component, the server-side timing will not appear in the same trace you recorded on the profiling build.
The loop, not the fix
The durable skill is the loop: baseline on the profiling build, read Scheduler before Components, use actualDuration versus baseDuration to pick the category of fix, change exactly one thing, re-record, and keep the change only if the trace shows the work moved or shrank. A 340ms long task is not a bug report until you can point at the renders inside it and say which lane they ran on and why they rendered. Everything before that point is guessing with a flame chart.
FAQ
Why don’t I see a Components track in my trace?
Three likely causes. You are on a production build, where the instrumentation is disabled by default. You are on a profiling build without the React DevTools extension, in which case only <Profiler>-wrapped subtrees are listed. Or Show custom tracks is off in Capture settings.
Can I use these tracks in production?
Not with the default production build. The profiling instrumentation adds overhead and is disabled there by default; React documents a special production build with profiling enabled as the opt-in path. Chrome’s console.timeStamp API is designed for low-overhead instrumentation suitable for hot paths, but that is a separate mechanism you would wire up yourself.
Should I trust the millisecond numbers from the development build?
No. The development build is instrumented for warnings and its absolute timings are not evidence for optimization decisions. Use the profiling build for numbers you intend to quote, and prefer the delta between two recordings over any single absolute value.
Does a wide bar on the Components track mean that component is slow?
Not necessarily. Each entry represents the duration of that component render and all its descendant children, so a wide bar near the root is inclusive time. Look at the leaves and at the actualDuration versus baseDuration comparison before concluding where the cost lives.