The Scheduler: Lanes, Priorities, and Interruptibility
If you have only ever thought of React as “a function from state to UI,” concurrent rendering will feel like a plot twist. The framework still aims for that simple equation, but the order and interruptibility of how that function is evaluated changed. React can start updating the tree, discover that something more important happened, and discard in-progress work to respond first. That behavior is not magic and it is not a separate mode you toggle in production for fun; it is the scheduler doing its job.
Understanding the scheduler is what lets senior engineers reason about hooks like useTransition
without cargo-culting them. Those hooks are not performance fairy dust—they are levers on
priority.
One renderer, many queues of work
At a high level, React maintains a priority system often described in terms of lanes. You do not
import lanes in application code, but they explain why two setState calls in the same event
handler can feel different. Some updates are treated as synchronous with user perception: they
should land before paint if possible. Others are allowed to wait, be interrupted, or be restarted.
A useful mental model maps user intent to coarse lane families:
- SyncLane — Work that must complete immediately for correctness or for matching platform expectations around discrete input. Think pointer events where the DOM and React state must agree before the user sees the next frame.
- InputContinuousLane — Work tied to continuous input: typing, dragging, scrolling. The browser is already delivering a stream of events; React tries to keep this work responsive so the UI does not fall behind the hardware event rate.
- TransitionLane — Work marked as non-urgent. This is where deferred updates live—filtering a large list, swapping tabs that mount heavy subtrees, animating a chart from a new query. If the user keeps typing, React may abort a partially completed transition render and start again with fresher state.
- IdleLane — Work that can wait until the browser is quiet—lowest priority housekeeping.
Exact lane names and internal merging rules are implementation details that evolve between React versions. The stable idea for application developers is simpler: React distinguishes urgent updates (discrete actions, continuous input) from transition updates (everything you wrapped or implied as deferrable). The scheduler uses that distinction to choose what to finish now versus what can wait.
Interruptibility: the ghost render you never committed
Classic React (pre-concurrent defaults) often framed rendering as atomic: once a render started, it ran to completion before the next one could begin. Concurrent rendering introduces interruptible renders. Suppose a transition begins reconciling a large subtree. Midway through, the user types another character. The in-flight transition work may be invalidated because its inputs are already stale. React throws away that partial attempt and prioritizes the input.
That is not a failed render in the error sense; it is the scheduler refusing to spend more CPU finishing pixels the user has already superseded. The trade is subtle but important:
- Good: The text field stays instant; the heavy list never blocks typing.
- Cost: You may do more total render work in wall-clock time, including speculative work that never commits. In pathological cases, aggressive concurrent patterns can increase render counts compared to a single blocking update.
This is why “concurrent” is not a synonym for “faster.” It can be faster for the user’s perception while being neutral or worse for total work done. Senior teams measure both: interaction latency (INP-style thinking) and wasted CPU (battery, low-end devices).
Urgent versus non-urgent: who decides?
React applies heuristics to events and updates, but you steer the system with APIs:
- Updates inside native event paths for typing and clicking tend to be treated as urgent unless you opt out.
- Updates wrapped in
startTransition(or otherwise classified as transitions) slide into lower-priority lanes.
The mental model: urgent updates must converge quickly with what the user is doing right now. Non-urgent updates are still correct, but their timeliness is negotiable. If they arrive late, the UI is still truthful; if they are restarted, only efficiency suffers—not correctness.
When concurrent scheduling helps—and when it does not
A blunt truth that saves a lot of churn: most state updates do not need concurrent scheduling. If your render is cheap, batching and a straightforward update already finish within a frame. Adding transitions everywhere buys complexity and can introduce extra renders for no perceptible gain.
Reach for concurrent patterns when at least one of these is true:
- A child tree is expensive to reconcile (large lists, complex charts, rich markdown).
- Updates are frequent relative to render cost (fast typing against a heavy derived view).
- You care about keeping input latency low more than minimizing total reconcile count.
Avoid leaning on the scheduler when:
- Render work is trivial; you are optimizing noise.
- Correctness requires strict ordering that spans multiple interdependent states—transitions batch conceptually, but splitting atomic business logic across priorities without care can produce odd intermediate UIs unless you design for them.
- You are chasing raw throughput benchmarks; concurrent mode optimizes time-to-interaction, not spreadsheet wins on a single synchronous update.
Scheduling versus “making React faster”
It helps to separate three different layers:
- Algorithmic efficiency — Better data structures, fewer O(n) passes, moving work out of render.
- Commit-phase performance — DOM mutations, layout thrash, CSS costs.
- Scheduling — Which updates run before the next paint, which can wait, which can be abandoned.
Concurrent features live almost entirely in (3). They can dramatically improve how snappy the app feels because the main thread returns to the browser sooner for urgent input. They do not replace profiling hot components or fixing accidental O(n²) renders.
TypeScript and mental models at code review
When reviewing concurrent code in TypeScript codebases, ask:
- Which state must track the user’s keystrokes pixel-for-pixel? That state should update urgently.
- Which state is derived presentation (filtered rows, sorted columns, preview HTML)? That is a prime candidate for transition or deferred value patterns in later sections.
- Is there a meaningful pending state? Interruptible work often deserves UI affordances: subtle opacity, a spinner, or “Updating…” copy—signals that the expensive view may lag slightly behind the input.
If the team cannot answer those questions, useTransition becomes syntax without semantics. The
scheduler section exists so those answers are grounded in how React chooses what to run, not in
blog-post slogans.
Closing the loop
Lanes and interruptibility are the spine of concurrent React. Everything else—startTransition,
deferred values, Suspense boundaries cooperating with the scheduler—is user-facing syntax over the
same principle: preserve urgency for input, defer or discard work that is already outdated.
Carry that mental model forward, and the hooks stop looking like a checklist and start looking like
precise tools with known costs.