Scheduler Priorities & Task Queue
The standalone scheduler turns priority-tagged callbacks into executable work. It assigns each task an expiration time, places delayed work in a timer heap or ready work in a task heap, and repeatedly runs the earliest eligible task.
The reconciler supplies event and lane priorities, which are translated into Scheduler priority levels before callbacks are scheduled. The scheduler then uses a host callback and a short time slice so unfinished work can yield and continue later.
Sources: packages/scheduler/src/forks/Scheduler.js:335-427, packages/scheduler/src/forks/Scheduler.js:79, packages/scheduler/src/forks/Scheduler.js:80, packages/react-reconciler/src/ReactFiberRootScheduler.js:481-498, packages/scheduler/src/forks/Scheduler.js:498-528
Core concepts
Scheduler priorities
A scheduler priority is an ordered numeric level: NoPriority is 0, ImmediatePriority is 1, UserBlockingPriority is 2, NormalPriority is 3, LowPriority is 4, and IdlePriority is 5.
Sources: packages/scheduler/src/SchedulerPriorities.js:13, packages/scheduler/src/SchedulerPriorities.js:14, packages/scheduler/src/SchedulerPriorities.js:15, packages/scheduler/src/SchedulerPriorities.js:16, packages/scheduler/src/SchedulerPriorities.js:17, packages/scheduler/src/SchedulerPriorities.js:18
Expiration time
An expiration time is the task’s startTime plus a timeout selected from its priority; an expired task is eligible even when the normal time-slice deadline has been reached.
| Priority | Timeout behavior | sortIndex for ready work |
|---|---|---|
ImmediatePriority | Times out immediately with -1 | expirationTime |
UserBlockingPriority | Uses userBlockingPriorityTimeout, 250 | expirationTime |
NormalPriority | Uses normalPriorityTimeout, 5000 | expirationTime |
LowPriority | Uses lowPriorityTimeout, 10000 | expirationTime |
IdlePriority | Uses maxSigned31BitInt, so it does not practically time out | expirationTime |
These values show why higher urgency is represented by an earlier expiration, while delayed tasks temporarily use their startTime for ordering.
Sources: packages/scheduler/src/forks/Scheduler.js:335-427, packages/scheduler/src/forks/Scheduler.js:193-266, packages/scheduler/src/SchedulerFeatureFlags.js:13, packages/scheduler/src/SchedulerFeatureFlags.js:14, packages/scheduler/src/SchedulerFeatureFlags.js:15, packages/scheduler/src/forks/Scheduler.js:76
Min-heap queue
A min-heap keeps its smallest node at index zero, and compare orders nodes first by sortIndex and then by task id. Tasks enter through push, while pop removes the first node and repairs the heap with siftDown.
Sources: packages/scheduler/src/SchedulerMinHeap.js:91-95, packages/scheduler/src/SchedulerMinHeap.js:17-21, packages/scheduler/src/SchedulerMinHeap.js:27-40
Event priority and lane
The reconciler represents event priority with lanes: DiscreteEventPriority maps to SyncLane, ContinuousEventPriority to InputContinuousLane, DefaultEventPriority to DefaultLane, and IdleEventPriority to IdleLane. eventPriorityToLane returns the event priority as the lane, while lanesToEventPriority examines the highest-priority lane and classifies it as discrete, continuous, default, or idle.
Sources: packages/react-reconciler/src/ReactEventPriorities.js:25, packages/react-reconciler/src/ReactEventPriorities.js:26, packages/react-reconciler/src/ReactEventPriorities.js:27, packages/react-reconciler/src/ReactEventPriorities.js:28, packages/react-reconciler/src/ReactEventPriorities.js:51-53, packages/react-reconciler/src/ReactEventPriorities.js:55-67
Host time slice
The normal scheduler uses frameYieldMs set to 5, copies it into frameInterval, and measures elapsed time from startTime. shouldYieldToHost yields once elapsed time reaches the frame interval, or earlier when painting is requested and needsPaint is set.
Sources: packages/scheduler/src/SchedulerFeatureFlags.js:11, packages/scheduler/src/forks/Scheduler.js:456, packages/scheduler/src/forks/Scheduler.js:459-472, packages/scheduler/src/forks/Scheduler.js:474-479
How priorities become queued work
unstable_scheduleCallback first computes startTime, using an optional positive delay or the current time. It then selects a timeout from the priority, computes expirationTime, and creates a task with an initially unset sortIndex.
var expirationTime = startTime + timeout;
var newTask: Task = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};The important distinction is that delayed work is ordered by startTime in timerQueue, while immediately eligible work is ordered by expirationTime in taskQueue.
Evidence
- priority-inputpackages/scheduler/src/forks/Scheduler.js:335
- task-recordpackages/scheduler/src/forks/Scheduler.js:335
- timer-queuepackages/scheduler/src/forks/Scheduler.js:80
- timer-queuepackages/scheduler/src/forks/Scheduler.js:335
- task-queuepackages/scheduler/src/forks/Scheduler.js:79
- task-queuepackages/scheduler/src/forks/Scheduler.js:335
- host-callbackpackages/scheduler/src/forks/Scheduler.js:563
When a timer reaches its startTime, advanceTimers removes it from timerQueue, changes its sortIndex to expirationTime, and pushes it into taskQueue. If all work is delayed, requestHostTimeout arranges for handleTimeout to run after the earliest delay; handleTimeout advances timers and either requests a host callback or schedules the next timer.
Sources: packages/scheduler/src/forks/Scheduler.js:335-427, packages/scheduler/src/forks/Scheduler.js:79, packages/scheduler/src/forks/Scheduler.js:80, packages/scheduler/src/forks/Scheduler.js:103-126, packages/scheduler/src/forks/Scheduler.js:128-143
How the scheduler runs and yields
The host callback is requested only when no callback is already scheduled and work is not currently being performed. requestHostCallback starts the message loop by calling schedulePerformWorkUntilDeadline. The supplied excerpt exposes localSetImmediate, localSetTimeout, and the schedulePerformWorkUntilDeadline variable, but does not show the initializer that chooses between setImmediate, MessageChannel, and setTimeout; therefore the preference order cannot be established from these excerpts.
Evidence
- host-entrypackages/scheduler/src/forks/Scheduler.js:498
- flush-workpackages/scheduler/src/forks/Scheduler.js:145
- work-looppackages/scheduler/src/forks/Scheduler.js:193
- task-queuepackages/scheduler/src/forks/Scheduler.js:79
- task-queuepackages/scheduler/src/forks/Scheduler.js:193
- yield-checkpackages/scheduler/src/forks/Scheduler.js:459
- yield-checkpackages/scheduler/src/forks/Scheduler.js:193
- continuationpackages/scheduler/src/forks/Scheduler.js:193
performWorkUntilDeadline records startTime, calls flushWork, and schedules another host callback when more work remains; otherwise it stops the message loop. flushWork marks the scheduler as performing work, invokes workLoop, and restores the previous priority and work state in its cleanup path.
workLoop advances timers, selects the first task with peek, and runs its callback when it is not blocked by the time-slice check. A callback can return a continuation; in that case the continuation remains on the current task and the loop yields immediately. If the callback completes, the task is removed with pop; if work remains, the loop continues or schedules the next timer.
Sources: packages/scheduler/src/forks/Scheduler.js:335-427, packages/scheduler/src/forks/Scheduler.js:563-568, packages/scheduler/src/forks/Scheduler.js:100-101, packages/scheduler/src/forks/Scheduler.js:97, packages/scheduler/src/forks/Scheduler.js:530, packages/scheduler/src/forks/Scheduler.js:498-528, packages/scheduler/src/forks/Scheduler.js:145-191, packages/scheduler/src/forks/Scheduler.js:193-266
How reconciler priorities map to Scheduler priorities
The reconciler converts the highest-priority lane into an event priority with lanesToEventPriority. Discrete and continuous event priorities both become UserBlockingSchedulerPriority, default event priority becomes NormalSchedulerPriority, and idle event priority becomes IdleSchedulerPriority.
The mapping intentionally does not use Scheduler ImmediatePriority for this path: the shown reconciler code says synchronous work now uses microtasks, while work reaching this path is intended to be time sliced.
Sources: packages/react-reconciler/src/ReactFiberRootScheduler.js:481-498, packages/react-reconciler/src/ReactFiberRootScheduler.js:481-487
What changes in the postTask fork
The postTask fork maps ImmediatePriority and UserBlockingPriority to browser 'user-blocking', NormalPriority and LowPriority to 'user-visible', and IdlePriority to 'background'. It creates a TaskController, passes its signal and optional delay to scheduler.postTask, and returns a controller-backed node.
Evidence
- callerpackages/scheduler/src/forks/SchedulerPostTask.js:74
- controllerpackages/scheduler/src/forks/SchedulerPostTask.js:74
- browser-schedulerpackages/scheduler/src/forks/SchedulerPostTask.js:45
- browser-schedulerpackages/scheduler/src/forks/SchedulerPostTask.js:74
- task-runnerpackages/scheduler/src/forks/SchedulerPostTask.js:118
- callbackpackages/scheduler/src/forks/SchedulerPostTask.js:118
runTask sets a deadline to getCurrentTime() + yieldInterval, where yieldInterval is 5, invokes the callback, and schedules a returned continuation with scheduler.yield when available or scheduler.postTask otherwise. unstable_shouldYield compares the current time with that deadline, while unstable_requestPaint is a no-op because this fork yields every frame.
Sources: packages/scheduler/src/forks/SchedulerPostTask.js:74-116, packages/scheduler/src/forks/SchedulerPostTask.js:118-168, packages/scheduler/src/forks/SchedulerPostTask.js:61-63, packages/scheduler/src/forks/SchedulerPostTask.js:65-67
How it connects
The reconciler’s lane model supplies the event priority that selects a Scheduler priority, so scheduling is the hand-off between Lanes & Priority and this package. The resulting callback drives the interruptible render work described in Work Loop & Scheduling.
The scheduler’s host callback is part of the browser-facing runtime boundary used by the renderer, while the postTask fork delegates execution to the browser’s scheduler.postTask. The broader package placement and renderer relationships are described in Repository Map.
Sources: packages/react-reconciler/src/ReactFiberRootScheduler.js:481-498, packages/scheduler/src/forks/Scheduler.js:193-266, packages/scheduler/src/forks/Scheduler.js:563-568, packages/scheduler/src/forks/SchedulerPostTask.js:74-116
Key takeaways
ImmediatePrioritythroughIdlePriorityuse numeric levels 1 through 5, with priority-specific expiration timeouts.- Delayed tasks use
startTimeintimerQueue; ready tasks useexpirationTimeintaskQueue. frameYieldMsis 5ms, andshouldYieldToHostyields after the frame interval or when painting is requested.- Reconciler discrete and continuous priorities map to user-blocking Scheduler work; default and idle map to normal and idle work.
- The postTask fork uses browser task priorities,
TaskController, and deadline-based continuation scheduling instead of the shown heap-driven host loop.
Sources: packages/scheduler/src/SchedulerPriorities.js:14, packages/scheduler/src/SchedulerPriorities.js:18, packages/scheduler/src/forks/Scheduler.js:335-427, packages/scheduler/src/SchedulerFeatureFlags.js:11, packages/scheduler/src/forks/Scheduler.js:459-472, packages/react-reconciler/src/ReactFiberRootScheduler.js:481-498, packages/scheduler/src/forks/SchedulerPostTask.js:74-116, packages/scheduler/src/forks/SchedulerPostTask.js:118-168