Work Loop & Scheduling
React’s reconciler turns root updates into scheduled rendering work, then chooses whether to render synchronously or through the concurrent work loop. The root scheduler keeps roots in a linked schedule and arranges a later microtask to decide what each root should do.
This separation exists so rendering can yield without committing a partial tree. Synchronous work is flushed at the end of the microtask, while other work becomes a Scheduler callback that can resume after the browser regains control.
Sources: packages/react-reconciler/src/ReactFiberRootScheduler.js:116-141, packages/react-reconciler/src/ReactFiberRootScheduler.js:384-393
Core concepts
Root schedule
A root schedule is the linked list of roots that currently have pending work; ensureRootIsScheduled adds a root and ensures that the schedule will be processed.
Sources: packages/react-reconciler/src/ReactFiberRootScheduler.js:116-152
Lane choice
A lane is the priority class selected for an update; requestUpdateLane maps legacy, render-phase, transition, and event updates to different lanes.
Sources: packages/react-reconciler/src/ReactFiberWorkLoop.js:812-868
Time slicing
Time slicing means rendering uses renderRootConcurrent instead of renderRootSync when work is not forced synchronous, blocking, or expired.
Sources: packages/react-reconciler/src/ReactFiberWorkLoop.js:1165-1181
Work-in-progress stack
The work-in-progress stack is the partially built render state held by workInProgressRoot, workInProgressRootRenderLanes, and workInProgress; it lets a later render continue or restart from a known state.
Sources: packages/react-reconciler/src/ReactFiberWorkLoop.js:439, packages/react-reconciler/src/ReactFiberWorkLoop.js:441, packages/react-reconciler/src/ReactFiberWorkLoop.js:443
How an update becomes scheduled work
An update first reaches scheduleUpdateOnFiber, which marks the root as updated and eventually calls ensureRootIsScheduled. The root scheduler appends the root to the scheduled-root list unless it is already present, sets mightHavePendingSyncWork, and calls ensureScheduleIsScheduled.
ensureScheduleIsScheduled schedules one immediate root-schedule task for the current event. In development, an active act scope uses a separate scheduling flag; otherwise the normal microtask flag prevents duplicate scheduling.
The microtask clears the scheduling flags, visits every root, and calls scheduleTaskForRootDuringMicrotask. Roots with no remaining work are removed from the linked list; roots with work remain scheduled.
The following workflow shows the root-level decision path.
Evidence
- fiber-updatepackages/react-reconciler/src/ReactFiberWorkLoop.js:987
- root-schedulerpackages/react-reconciler/src/ReactFiberRootScheduler.js:116
- schedule-microtaskpackages/react-reconciler/src/ReactFiberRootScheduler.js:154
- root-microtaskpackages/react-reconciler/src/ReactFiberRootScheduler.js:259
- lane-selectionpackages/react-reconciler/src/ReactFiberRootScheduler.js:384
- sync-pathpackages/react-reconciler/src/ReactFiberRootScheduler.js:608
- scheduler-pathpackages/react-reconciler/src/ReactFiberRootScheduler.js:626
The lane decision has three important outcomes.
| Mode | Condition | Result |
|---|---|---|
| Synchronous | includesSyncLane(nextLanes) and the root is not prerendering | The root is marked SyncLane; no extra Scheduler task is needed. |
| Reused callback | The highest-priority lane matches root.callbackPriority | The existing task is reused. |
| Rescheduled callback | The priority changed or an act task must replace a Scheduler task | The old callback is cancelled and a new callback is scheduled. |
Sources: packages/react-reconciler/src/ReactFiberWorkLoop.js:1026-1027, packages/react-reconciler/src/ReactFiberRootScheduler.js:124-141, packages/react-reconciler/src/ReactFiberRootScheduler.js:154-169, packages/react-reconciler/src/ReactFiberRootScheduler.js:259-331, packages/react-reconciler/src/ReactFiberRootScheduler.js:441-480
How React chooses sync or concurrent rendering
At the synchronous end of the microtask, flushSyncWorkAcrossRoots_impl walks the scheduled roots and calls performSyncWorkOnRoot when a root has synchronous work. It repeats while any root performed work, preventing pending synchronous roots from being left behind.
performSyncWorkOnRoot flushes pending effects before entering performWorkOnRoot with forceSync set to true. Inside performWorkOnRoot, time slicing is enabled only when the task is not forced synchronous, does not include a blocking lane, and has not expired; prerendering is an exception because it uses the concurrent loop to avoid blocking the main thread.
The render branch is therefore explicit: renderRootConcurrent can leave the root in RootInProgress, while renderRootSync runs without yielding between fibers.
Evidence
- sync-entrypackages/react-reconciler/src/ReactFiberRootScheduler.js:608
- root-workpackages/react-reconciler/src/ReactFiberWorkLoop.js:1137
- concurrent-renderpackages/react-reconciler/src/ReactFiberWorkLoop.js:1165
- sync-renderpackages/react-reconciler/src/ReactFiberWorkLoop.js:1165
- unit-workpackages/react-reconciler/src/ReactFiberWorkLoop.js:3081
- complete-workpackages/react-reconciler/src/ReactFiberWorkLoop.js:3368
Sources: packages/react-reconciler/src/ReactFiberRootScheduler.js:185-247, packages/react-reconciler/src/ReactFiberRootScheduler.js:608-622, packages/react-reconciler/src/ReactFiberWorkLoop.js:1165-1181
How yielding and resumption work
When concurrent rendering reaches its time limit, performWorkOnRoot leaves the render in progress, records the suspended reason for yield timing, and returns to the event loop. When work enters again, the same function detects an existing workInProgressRootRenderLanes and non-null workInProgress, then records the time spent yielded before continuing.
The Scheduler keeps this host-level loop moving: performWorkUntilDeadline calls flushWork, and schedules another deadline message when flushWork reports more work. flushWork delegates task processing to workLoop while preserving the current priority and clearing the current task when the host callback finishes.
An interruption does not necessarily discard all progress. If the root and lanes are unchanged, renderRootSync continues with the existing stack; if either changes, it calls prepareFreshStack and starts a new stack. An incoming update can explicitly interrupt suspended work: scheduleUpdateOnFiber calls prepareFreshStack, marks the previous lanes suspended, then marks the new lane updated.
If a fiber throws while rendering, handleThrow records whether the cause is data, an action, hydration, or an error, and throwAndUnwindWorkLoop unwinds toward a boundary or resumes at a sibling. Normal completion advances to a child, sibling, or parent and marks the root RootCompleted when no work remains.
Sources: packages/react-reconciler/src/ReactFiberWorkLoop.js:1185-1207, packages/react-reconciler/src/ReactFiberWorkLoop.js:1146-1161, packages/scheduler/src/forks/Scheduler.js:498-525, packages/scheduler/src/forks/Scheduler.js:145-191, packages/react-reconciler/src/ReactFiberWorkLoop.js:2633-2654, packages/react-reconciler/src/ReactFiberWorkLoop.js:1004-1027, packages/react-reconciler/src/ReactFiberWorkLoop.js:3234-3327, packages/react-reconciler/src/ReactFiberWorkLoop.js:3368-3431
How it connects
The reconciler hands non-synchronous callbacks to the standalone Scheduler through scheduleCallback; in development, an active act scope instead pushes the callback onto the internal act queue. The Scheduler’s task queue is a min-heap: push inserts with siftUp, peek reads the first task, and pop removes it with siftDown. See Scheduler Priorities & Task Queue for the Scheduler’s priority levels, task queue, and host-yield policy.
The render phase builds work through beginWork, performUnitOfWork, and completeUnitOfWork; completed work then enters the commit machinery through pending effects. Mutation effects update the host tree before layout effects run, and root.current switches to the finished tree after mutation effects. See Render Phase and Commit Phase.
Lane assignment is shared with the update-priority model: transitions reuse one lane within an event through requestTransitionLane, while requestUpdateLane selects the lane used by scheduling. See Lanes & Priority and Concurrent Features & Transitions.
Sources: packages/react-reconciler/src/ReactFiberWorkLoop.js:5584-5599, packages/scheduler/src/SchedulerMinHeap.js:17-21, packages/scheduler/src/SchedulerMinHeap.js:23-25, packages/scheduler/src/SchedulerMinHeap.js:27-40, packages/react-reconciler/src/ReactFiberWorkLoop.js:3081-3123, packages/react-reconciler/src/ReactFiberWorkLoop.js:3368-3431, packages/react-reconciler/src/ReactFiberWorkLoop.js:4009-4052, packages/react-reconciler/src/ReactFiberRootScheduler.js:698-724, packages/react-reconciler/src/ReactFiberWorkLoop.js:812-868
Key takeaways
- Root updates enter
ensureRootIsScheduled, which maintains the root list and arranges a microtask. scheduleTaskForRootDuringMicrotaskdecides whether to flush synchronous work or schedule a callback.- Concurrent rendering yields with
RootInProgressand later re-enters using the work-in-progress state. - Changed lanes or an interrupting update can cause
prepareFreshStackto restart rendering. - The Scheduler keeps unfinished host work moving by calling
flushWorkand scheduling another deadline when more work remains.
Sources: packages/react-reconciler/src/ReactFiberRootScheduler.js:116-152, packages/react-reconciler/src/ReactFiberRootScheduler.js:384-509, packages/react-reconciler/src/ReactFiberWorkLoop.js:1185-1207, packages/react-reconciler/src/ReactFiberWorkLoop.js:2633-2654, packages/scheduler/src/forks/Scheduler.js:498-525