A React 19 app wraps a state update in startTransition, but the page still freezes for roughly 150 ms on every update because one component's render function runs an expensive synchronous computation. Why does marking the update as a transition fail to keep the page responsive here?
answer
- priority is about scheduling, not duration
- the check sits between units, never inside one
- one component call is the smallest slice
- spread cost yields well, concentrated cost does not
- get the work off the render path
basics
~20 sReact can only yield between units of work, and one component's render call is a single indivisible unit. A transition lets React interrupt the tree walk, not a function already running, so a 150 ms component body still blocks the frame.
solid answer
~50 sTransitions change *when* React does the tree walk and whether it can be interrupted — they do not make any individual piece of work preemptible. React's work loop checks whether it should yield only between units of work, and one unit is one node, which for a function component means one full call of that function. Because JavaScript runs a call to completion, nothing can stop your 150 ms body once it starts, so the frame is blown regardless of priority. Concurrent rendering helps when cost is spread thinly across many components; it does nothing when the cost is concentrated in one. The fix is to get the computation out of the render path — move it to a worker, compute it incrementally across updates, or reduce what has to be computed per render — rather than to reach for a different scheduling API.
code
javascript · 13 linesfunction expensiveSummarize(rows) {
let total = 0;
for (const row of rows) {
for (let i = 1; i < 20000; i++) total += row.value % i;
}
return total;
}
function Report({ rows }) {
// One unit of work: React cannot yield anywhere inside this call.
const total = expensiveSummarize(rows);
return <output>{total}</output>;
}go deeper
Remember the boundary: React can stop between components, never inside one, so a slow function still blocks the page no matter how the update was scheduled.
Explain that the yield check sits between units of work and that a unit includes the whole component call, so run-to-completion sets the granularity floor.
Diagnose it from a profile and prescribe real fixes — take the work off the render path, move it to a worker, or chunk it — rather than reaching for another scheduling API.
Own the framing for the team: the framework controls scheduling, you control the cost of a single call, and treating concurrency features as a cure for expensive code is a recurring architectural mistake worth naming in review.
## What the transition actually promised Marking an update as a transition tells React that this update is not urgent: React may render it in the background, may interrupt that render if something urgent arrives, and may abandon and redo it. What it never promised is that any individual piece of work becomes interruptible. Priority decides *which* work runs and *whether the walk can be stopped between steps*; it has no power over a function that is already executing. ## Where the yield points are React's concurrent work loop looks conceptually like this: ```js while (workInProgress !== null && !shouldYield()) { workInProgress = performUnitOfWork(workInProgress); } ``` The check happens between iterations. Each iteration is one unit of work — one node of the work tree — and for a function component that unit includes calling your component function and reconciling what it returns. There is no check inside `performUnitOfWork`, and there could not be: JavaScript runs a call to completion, so once React has entered your component body, neither React nor the browser can take the thread back until it returns. So the yield granularity is exactly "one component render". React converts one enormous indivisible block into many small indivisible blocks. If the smallest block is itself 150 ms, the conversion has bought nothing. ## Reading the symptom correctly The telltale distinction is where the time is spent: - **Cost spread across the tree** — ten thousand cheap components, each a fraction of a millisecond. React yields every few milliseconds, input stays responsive, and the update simply lands over several frames. This is what concurrent rendering is for. - **Cost concentrated in one node** — one component that sorts a huge array, formats a giant string, or parses something on every render. React reaches that node, yields nothing for 150 ms, and drops frames. No scheduling API fixes it. A profiler makes this obvious: in the first case the long task is chopped into slices; in the second there is one long task with a single component's render dominating it. ## What actually fixes it Since the constraint is that a single call cannot be split by the scheduler, every real fix either removes the work from the render path or splits it yourself: - **Do not compute it during render.** If the result depends only on inputs that rarely change, compute it once when those inputs change rather than on every pass. - **Move it off the main thread.** A Web Worker runs the computation on another thread and posts the result back; the component then renders a cheap value. This is the only option that removes the cost from the frame entirely. - **Chunk it yourself.** Compute a slice per pass and keep partial results in state, so each render does bounded work — you are re-creating, at your own granularity, what React does between fibers. - **Compute less.** Often the honest answer: the component is formatting ten thousand rows when only the visible slice matters. Note what is *not* a fix: switching to a different scheduling hook, wrapping the update differently, or forcing a synchronous flush. All of them still have to run your function once, in full, on the main thread. ## The distinction interviewers are testing This question separates people who have used a concurrent API from people who know what it changes in the pipeline. The framework's leverage is over *scheduling* — when work starts, in what order, whether it can be dropped. It has no leverage over the *duration* of a single synchronous call. If your component takes 150 ms, every framework on the planet will take 150 ms, and the responsibility to break that up is yours. ## A related trap The same reasoning explains why a slow render is not made safe by lowering its priority for the user's benefit: a background render still occupies the main thread while it runs. Lower priority means the browser gets the thread back *between* units and urgent work jumps ahead — it does not mean the work happens somewhere else.
- How would you confirm from a performance profile that the problem is one component rather than a large tree?Look at the shape of the long task. A wide tree shows many short render entries that React chops across several slices, with yield gaps between them. A single hot component shows one long uninterrupted task dominated by one render entry, with no gaps — that is the signature of work React could not split, and it points you at that component's body.
- Would splitting that component into many smaller child components give React more places to yield?Only if the expensive computation is genuinely divided among them. Splitting the JSX while one child still performs the whole calculation just moves the same indivisible 150 ms call. More components create more yield points, but a yield point between cheap nodes does not help when one node remains huge — the work itself has to be partitioned or moved off the main thread.
- Does moving the computation into a Web Worker introduce any behaviour you have to design around?Yes — the result now arrives asynchronously, so the component must render some intermediate state first and update when the message returns, and you must handle results arriving out of order or after the inputs changed again. You trade a blocked frame for the usual asynchronous concerns: a pending state, cancellation of stale results, and structured-clone limits on what you can post.
saying these in an interview costs you the question
- Thinks a transition makes any long computation interruptible
- Believes React can pause inside a running component function
- Says lower priority moves work off the main thread
- Expects a different scheduling hook to fix a slow function
- Confuses spreading render cost with reducing render cost