During a concurrent render, React's scheduler stops partway through, hands control back to the browser, and continues the render in a later task. What decides when it yields, how does it get control back, and what unit of work can it never break up?
answer
- React stops only at certain boundaries
- between components, never inside one
- a small elapsed-time budget, not a frame
- continuation comes back as a task, not a microtask
basics
~20 sReact checks between units of work — roughly one component each — whether the current slice has used its small time budget. If so it stops and posts a browser task to continue later. It can never interrupt the inside of one component function.
solid answer
~50 sReact renders one unit of work at a time, where a unit is roughly one component, and between units it asks the scheduler whether it should yield. The scheduler says yes once the current slice has run past a small budget — about five milliseconds in React 19's implementation, which is an internal detail rather than a public knob. React then stops and asks the scheduler to schedule the continuation as an ordinary browser task, so the browser can paint and deliver pending input before React resumes. What it can never split is a single component's function body: that is plain JavaScript and runs to completion. So the worst-case time React can hold the main thread is roughly the cost of the slowest single component, plus the commit phase, which is synchronous and never sliced.
go deeper
Know that React renders a bit at a time and gives the browser turns in between, and that it can only stop between components, never inside one.
Explain the three mechanics precisely: a yield check between units of work, a small elapsed-time budget as the trigger, and a posted browser task as the continuation.
Use the granularity limit diagnostically — when an app still janks under a transition, identify the single expensive component body or the large commit as the uninterruptible stretch and fix that instead of adding priority.
Treat the slice budget and commit cost as a latency budget the architecture has to respect, and set expectations that scheduling reorders work while only structural changes reduce it.
## The yield check sits between units of work React's render phase is a loop over units of work. Each unit is essentially one fiber — the internal object backing one component or host element — and processing it means calling the component function (or preparing the host element) and moving on to the next node in the tree. The loop used for concurrent-priority renders differs from the synchronous one in exactly one respect: before taking the next unit, it asks the scheduler whether it should yield. That check is cheap and it is the *only* place React can stop. It cannot stop halfway through your component body, because your component body is ordinary JavaScript and the engine runs it to completion. ## What makes the answer "yes, yield" The scheduler tracks when the current slice started. Once the slice has run longer than a small budget, the next check returns true. In React 19's `scheduler` package that budget is about five milliseconds — short enough that the browser still has room inside a typical frame to do style, layout, paint, and input dispatch. Treat the number as an implementation detail: it is not configurable, it has changed historically, and an interviewer asking for it is asking whether you know it is *small and fixed*, not whether you memorised it. Two things follow. First, React can overshoot the budget by however long the current unit took, because the check happens *between* units. Second, low-priority work is not deferred to some idle moment — React keeps working in short bursts, so a transition still makes steady progress on a busy page. ## How React gets control back When it yields, React does not simply return and hope. The scheduler posts a task so the browser will call it back after it has had its turn. The current implementation posts that continuation through a `MessageChannel` port, falling back to `setTimeout` where `MessageChannel` is unavailable. The important property is that it is a *task*, not a microtask: microtasks drain before the browser can paint or dispatch input, so continuing on a microtask would defeat the whole purpose. It is also not tied to `requestAnimationFrame` or `requestIdleCallback` — React does not wait for a frame boundary or for the browser to declare itself idle. ## The floor you cannot slice below Because a unit of work is one component, the granularity of slicing is bounded by the cost of your most expensive single component: ```jsx function Report({ rows }) { // 200 ms of synchronous work in one body: one unit of work, // one uninterruptible block, regardless of priority. const summary = expensiveAggregate(rows); return <pre>{summary}</pre>; } ``` No priority setting improves that. The fixes are structural: split the work across more, smaller components so there are more yield points; move the computation out of render (precompute it, or do it in a worker and feed the result in as state); or render less of it at once. The commit phase is the other uninterruptible stretch. Once a render completes, React applies DOM mutations and runs layout effects synchronously so the user never observes a half-applied tree. A concurrent render therefore buys you interruptibility during *render*, and nothing during *commit* — which is one reason a huge commit (thousands of new DOM nodes) can still drop frames even when the render was smoothly sliced. ## Synchronous renders are not sliced at all Slicing applies to concurrent-priority work. An urgent update, or anything wrapped in `flushSync`, renders start to finish without yielding, because the point of urgency is to be on screen at the next paint. This is why "React 18 made everything interruptible" is an overstatement worth correcting: React 18 made everything render through the concurrent renderer, but urgent lanes deliberately do not yield. ## How to answer Say the three parts explicitly: yielding is checked between components; the trigger is a small elapsed-time budget, not a frame boundary or an idle callback; and the continuation comes back as a browser task so paint and input can happen in between. Then name the limit — one component body is atomic, and the commit is atomic — because that is the part that predicts real behaviour when someone asks why their app still janks.
- Why does the scheduler continue on a task rather than a microtask?Microtasks drain before the browser regains control, so a microtask continuation would run immediately after React yielded and no paint or input dispatch could happen in between — the yield would be purely nominal. Posting a real task lets the browser take its turn first, which is the entire point of slicing.
- Would splitting a 200 ms component into ten components of 20 ms each fix the jank?It helps in the specific sense that React now has ten yield points instead of one, so the worst-case uninterrupted block drops from 200 ms to about 20 ms and input latency improves. It does not reduce the 200 ms of total work, so if the goal is a faster result rather than a responsive one, the computation itself has to get cheaper or move off the render path.
- Does an urgent update get time sliced too?No. Slicing applies to concurrent-priority work such as transitions. Urgent updates and anything inside `flushSync` render to completion without yielding, because delaying them across tasks would defeat the reason they are urgent. That is why a very expensive urgent render still blocks the frame outright.
saying these in an interview costs you the question
- Says React yields at frame boundaries via requestAnimationFrame
- Claims React can pause inside a component function
- Thinks low-priority work only runs during browser idle time
- Assumes the commit phase is sliced like the render phase