| |

React 104 ⚛️ Concurrent Rendering Scheduler and Priorities

⏱️ Estimated read: 11 min | |

Concurrent rendering is not a feature that makes React faster. It is a scheduling model that changes when React does its work. The scheduler assigns a priority to every state update, processes the highest-priority work first, and yields to the browser between small slices of work so that user input is never blocked by a long render . The mechanism that makes this possible is the lane-based priority system introduced in React 18, which replaced the expiration-time model with a 31-bit bitmask where each bit represents a priority class .

Key point: The scheduler is cooperative, not parallel. It does not run work on multiple threads. It runs work in small chunks, checks after each chunk whether the browser needs to respond to something more urgent, and pauses if it does. The priority system determines which chunk runs next.


Why concurrent scheduling exists

The blocking problem. Before React 18, every state update triggered a synchronous render that ran to completion. A deeply nested component tree or an expensive computation could block the main thread for hundreds of milliseconds, preventing the browser from responding to clicks, keystrokes, or scroll events . The frame budget for 60 FPS is approximately 16 milliseconds, and a render that exceeds it drops frames, producing visible jank .

The priority problem. Not all updates are equally urgent. A keystroke in a search input must be reflected immediately, while the filtering of a large list based on that keystroke can wait. The pre-React 18 scheduler treated every update the same: once it started, it finished . Concurrent scheduling assigns different priorities to different update sources and allows a high-priority update to interrupt a lower-priority one .

The preemption problem. When a higher-priority update arrives during an in-flight lower-priority render, React discards the work-in-progress tree, processes the urgent update, and restarts the lower-priority work from the new state. This is preemption: it discards work rather than merging it . A long transition that is repeatedly preempted by sync updates can starve, so React includes starvation protection that escalates lanes that have waited too long to SyncLane so they eventually commit .


a. The Lane Model: Priority as a Bitmask

Lanes are a 31-bit bitmask where each bit (or group of bits) represents a priority class. The scheduler uses bitwise operations to check which lanes have pending work, which lanes are higher priority, and which lanes should be processed together .

LanePriorityUse Case
SyncLaneHighestDiscrete user input (click, keypress, change)
InputContinuousLaneHighContinuous input (drag, scroll, mouse-move)
DefaultLaneNormalPlain setState outside any scheduling primitive
TransitionLane (×16)LowstartTransition / useDeferredValue updates
RetryLaneLowRe-render after a Suspense boundary resolves
IdleLaneLowestBackground work (offscreen, prerendered routes)

The lane model replaced the expiration-time model because expiration times forced React to process updates strictly in order of expiration. Lanes give two new capabilities: batching (multiple updates of the same priority merge into a single render) and preemption (a higher-priority lane can interrupt an in-flight lower-priority render) .

function handleClick() {
  // SyncLane: committed immediately
  setCount(c => c + 1);

  // TransitionLane: interruptible, can be discarded and restarted
  startTransition(() => {
    setSearchResults(filterLargeList());
  });
}

When the user clicks during a slow transition, React discards the work-in-progress tree, processes the click on SyncLane, and then restarts the transition from the updated state . The transition’s previous work is not merged; it is restarted with the new state as input.


b. Time Slicing and the Work Loop

The scheduler does not run the entire render in one synchronous block. It splits render work into small windows—approximately 5 milliseconds each—and yields to the main thread between windows . After each window, the scheduler checks whether there is higher-priority work or whether the browser needs to respond to input. If so, it pauses the render and resumes it later.

// Simplified work loop
function workLoop() {
  while (workInProgress !== null && !shouldYield()) {
    workInProgress = performUnitOfWork(workInProgress);
  }
  // If shouldYield() returns true, React yields and resumes later
}

The shouldYield() check is the mechanism that keeps the main thread responsive. It allows the browser to process user input, run animations, and paint frames between slices of React work. The render phase is interruptible; the commit phase is not. Once the work-in-progress tree is complete and React begins committing, it applies all DOM changes in one synchronous pass .


c. Urgent, Transition, and Deferred Updates

The scheduler distinguishes three categories of work, each with a different priority and a different interruption behavior.

Urgent updates are reflected immediately. Input state, cursor position, toggles, and imperative UI changes are processed on SyncLane or InputContinuousLane. React commits them as soon as possible .

Transition updates are marked as non-blocking. startTransition and useTransition wrap updates that can be interrupted. React starts rendering them concurrently and may restart them many times if urgent work arrives. The previous UI remains visible until the transition reaches a commit-ready state .

Deferred values are downstream updates that adopt a slower, trailing version of some upstream state. useDeferredValue returns a value that lags behind the current value, allowing the expensive part of the tree to render with the old value while the urgent part renders with the new one. There is no fixed delay; the deferred render happens right after the urgent render is reflected on the screen, and it is interruptible .

The hierarchy is: urgent updates preempt transitions, and transitions preempt deferred values. The goal is not parallelism; it is controlled responsiveness under load .


Complete Example Session

// ============================================
// PART 1: SYNC LANE — URGENT INPUT
// ============================================
function SearchBox() {
  const [input, setInput] = useState('');

  const handleChange = (e) => {
    // SyncLane: committed immediately
    setInput(e.target.value);
  };

  return <input value={input} onChange={handleChange} />;
}


// ============================================
// PART 2: TRANSITION LANE — INTERRUPTIBLE
// ============================================
function SearchBox() {
  const [input, setInput] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  const handleChange = (e) => {
    setInput(e.target.value); // SyncLane

    startTransition(() => {
      // TransitionLane: can be interrupted
      setResults(filterLargeList(e.target.value));
    });
  };

  return (
    <div>
      <input value={input} onChange={handleChange} />
      {isPending && <Spinner />}
      <ResultsList results={results} />
    </div>
  );
}


// ============================================
// PART 3: DEFERRED VALUE — TRAILING STATE
// ============================================
function SearchBox() {
  const [input, setInput] = useState('');
  const deferredInput = useDeferredValue(input);

  // input renders urgently; deferredInput lags behind
  return (
    <div>
      <input value={input} onChange={e => setInput(e.target.value)} />
      <SlowList query={deferredInput} />
    </div>
  );
}


// ============================================
// PART 4: PREEMPTION — SYNC INTERRUPTS TRANSITION
// ============================================
// User types in input (SyncLane) during a transition
// React discards the transition's work-in-progress tree
// Processes the keystroke on SyncLane
// Restarts the transition from the new input state
// The transition's previous work is not merged


// ============================================
// PART 5: STARVATION PROTECTION
// ============================================
// A transition repeatedly preempted by sync updates
// can starve (never commit)
// React escalates lanes that have waited too long
// to SyncLane so they eventually commit


// ============================================
// PART 6: TIME SLICING — 5MS WINDOWS
// ============================================
// Render work is split into ~5ms windows
// After each window, shouldYield() checks:
// - Is there higher-priority work?
// - Does the browser need to respond?
// If yes, yield and resume later


// ============================================
// PART 7: RENDER PHASE YIELDS, COMMIT DOES NOT
// ============================================
// Render phase: interruptible
// Work-in-progress tree can be discarded
// Commit phase: atomic
// DOM changes applied in one synchronous pass


// ============================================
// PART 8: BATCHING — SAME PRIORITY MERGES
// ============================================
// Multiple updates of the same priority merge
// into a single render
// React 18+ batches across all sources:
// event handlers, promises, timeouts, native listeners


// ============================================
// PART 9: LANE BITMASK OPERATIONS
// ============================================
// root.lanes = pending work across the tree
// getHighestPriorityLane(root.lanes) → next lane
// includesSyncLane(lanes) → is there sync work?
// Lane operations are bitwise, not array iteration


// ============================================
// PART 10: SCHEDULER VS. RENDERER
// ============================================
// Scheduler: decides when work runs
// Renderer: decides what work to do
// The scheduler is generic; React DOM is one renderer
// The same scheduler could drive a different renderer

The ten parts trace the lane model, the three update categories, preemption, and the work loop. Part 4 is the critical preemption behavior: the sync update discards the transition’s work rather than merging it. Part 6 shows the time-slicing mechanism that keeps the main thread responsive.


Quick Reference

Lane Priority Classes

LanePriorityTypical Source
SyncLaneHighestClick, keypress, discrete input
InputContinuousLaneHighDrag, scroll, mouse-move
DefaultLaneNormalPlain setState
TransitionLaneLowstartTransition, useDeferredValue
RetryLaneLowSuspense retry
IdleLaneLowestBackground work

Update Categories

CategoryAPIInterruptible
UrgentsetState, event handlersNo (sync)
TransitionstartTransition, useTransitionYes
DeferreduseDeferredValueYes

Scheduler Behavior

MechanismEffect
Time slicing (~5ms)Yields between work units
PreemptionHigher lane discards lower work
Starvation protectionOld lanes escalated to SyncLane
BatchingSame-priority updates merged

Best Practices

✅ Do This:

// Use startTransition for non-urgent updates
startTransition(() => { setResults(filter(input)); });                  // ✅
// Use useDeferredValue for expensive downstream renders
const deferred = useDeferredValue(input);                               // ✅
// Keep urgent updates synchronous and small
setInput(e.target.value);                                               // ✅

❌ Don’t Do This:

// Wrap every state update in startTransition
// Urgent input becomes laggy                                          // ❌
// Expect the scheduler to optimize expensive render logic
// It schedules work; it does not make slow code fast                 // ❌
// Rely on a transition committing immediately
// It can be interrupted and restarted                                 // ❌

Common Pitfalls

PitfallWhy It HappensFix
Input lags during transitionTransition work not yieldingCheck that startTransition is used
Transition never commitsStarvation from repeated preemptionReact escalates to SyncLane eventually
Deferred value never updatesExpensive render blocks main threadProfile render cost; memoize
Batching not appliedUpdate outside React eventReact 18+ batches all sources

Real-World Examples

1. Urgent Input

setInput(e.target.value); // SyncLane

2. Transition List Filter

startTransition(() => { setResults(filter(input)); });

3. Deferred Value

const deferred = useDeferredValue(input);

4. Pending State

const [isPending, startTransition] = useTransition();

5. Lane Check

// root.lanes & SyncLane → sync work pending

6. Preemption

// Sync update discards transition's WIP tree

7. Time Slice

// ~5ms work window, then shouldYield()

8. Starvation Protection

// Old lane escalated to SyncLane

9. Batching

// Multiple setState calls in one event → one render

10. Scheduler Priority

// DiscreteEventPriority = SyncLane

Visual

The Lane Bitmask

┌──────────────────────────────────────────────────────────────┐
│  31-BIT LANE BITMASK                                         │
│                                                              │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ SyncLane        █                                       ││
│  │ InputContinuous ██                                      ││
│  │ Default         ████                                    ││
│  │ Transition      ████████████████                        ││
│  │ Retry           ████████████████████████                ││
│  │ Idle            ████████████████████████████████████    ││
│  └─────────────────────────────────────────────────────────┘│
│                                                              │
│  Each bit represents a priority class.                       │
│  root.lanes = pending work across the tree                   │
│  getHighestPriorityLane() finds the next lane to process     │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Preemption Flow

┌──────────────────────────────────────────────────────────────┐
│  TRANSITION RENDERING                                        │
│                                                              │
│  ┌─────────────────┐                                        │
│  │ startTransition │  TransitionLane work begins            │
│  │ filter(list)    │                                        │
│  └────────┬────────┘                                        │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │ Work in progress│  ~5ms slice                            │
│  └────────┬────────┘                                        │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │ User clicks     │  SyncLane update arrives               │
│  └────────┬────────┘                                        │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │ DISCARD WIP     │  Transition work abandoned             │
│  └────────┬────────┘                                        │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │ Process click   │  SyncLane commits immediately          │
│  └────────┬────────┘                                        │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │ RESTART         │  Transition re-runs with new state     │
│  │ transition      │                                        │
│  └─────────────────┘                                        │
│                                                              │
│  Preemption discards work; it does not merge it.             │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Time Slicing Work Loop

┌──────────────────────────────────────────────────────────────┐
│  WORK LOOP                                                   │
│                                                              │
│  ┌─────────────────┐                                        │
│  │ Process Fiber   │  One unit of work                      │
│  └────────┬────────┘                                        │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │ shouldYield()?  │────► YES: pause, let browser respond   │
│  │                 │      Resume later                       │
│  └────────┬────────┘                                        │
│           │ NO                                               │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │ Next Fiber      │                                        │
│  └────────┬────────┘                                        │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │ More work?      │────► YES: loop                         │
│  └────────┬────────┘                                        │
│           │ NO                                               │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │ Commit phase    │  Atomic, synchronous                   │
│  └─────────────────┘                                        │
│                                                              │
│  Render yields; commit does not.                             │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Update Priority Hierarchy

┌──────────────────────────────────────────────────────────────┐
│  URGENT (SyncLane)                                           │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ Input, click, cursor position                           ││
│  │ Preempts everything below                               ││
│  └─────────────────────────────────────────────────────────┘│
│           │                                                  │
│           ▼                                                  │
│  TRANSITION (TransitionLane)                                 │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ startTransition, list filtering                         ││
│  │ Interruptible, can be discarded and restarted           ││
│  └─────────────────────────────────────────────────────────┘│
│           │                                                  │
│           ▼                                                  │
│  DEFERRED (useDeferredValue)                                 │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ Trailing version of upstream state                      ││
│  │ Renders after urgent work is reflected                  ││
│  └─────────────────────────────────────────────────────────┘│
│                                                              │
│  Higher categories preempt lower categories.                 │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
Scheduler modelCooperative, interruptible, not parallel
Lane model31-bit bitmask, one bit per priority class
SyncLaneHighest; discrete input
TransitionLaneLow; startTransition
Time slice~5ms windows, yield between
PreemptionHigher lane discards lower WIP tree
Starvation protectionOld lanes escalated to SyncLane
Render phaseInterruptible
Commit phaseAtomic

Key takeaways:

  • Concurrent rendering changes when work runs, not how fast it runs. The scheduler does not make expensive render logic cheaper. It splits render work into small slices and yields to the browser between slices so that urgent input is never blocked .
  • Lanes are the priority mechanism. A 31-bit bitmask assigns different priorities to different update sources. SyncLane for discrete input, TransitionLane for startTransition, IdleLane for background work. The scheduler uses bitwise operations to find the highest-priority lane and process it first .
  • Preemption discards work rather than merging it. When a sync update arrives during a transition render, React discards the transition’s work-in-progress tree, processes the sync update, and restarts the transition from the new state. A transition that is repeatedly preempted can starve, so React escalates old lanes to SyncLane to guarantee eventual commit .
  • The render phase is interruptible; the commit phase is atomic. The work loop yields between units of render work. Once the tree is complete and React begins committing, it applies all DOM changes in one synchronous pass to avoid visual inconsistency .
  • Urgent, transition, and deferred updates form a hierarchy. Urgent updates preempt transitions, and transitions preempt deferred values. The goal is controlled responsiveness, not parallelism. startTransition and useDeferredValue are the APIs that let developers participate in this hierarchy .

Remember: The concurrent scheduler is not a performance optimization that makes slow code fast. It is a scheduling model that decides which work runs and when. The lane bitmask assigns priority, the work loop yields between slices, and preemption discards lower-priority work when urgent input arrives. The render phase is speculative and interruptible; the commit phase is definitive and atomic. The APIs that expose this model—startTransition, useTransition, useDeferredValue—are not ways to make work asynchronous in the traditional sense. They are ways to tell React that a particular update can be interrupted, restarted, or delayed. Understanding the lane model is understanding how React decides what to do next.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!