facebook/react
scheduler
Active contributors: acdlite, sebmarkbage, sophiebits, eps1lon
Purpose
packages/scheduler/ is a tiny cooperative task scheduler. It implements priority-based callbacks with MessageChannel (or postTask where available) so React can yield to the browser between fibers. The scheduler is published as scheduler on npm and is intentionally not React-specific — a few non-React projects depend on it directly.
The reconciler uses the scheduler for two things:
scheduleCallback(priority, fn)— "runfnlater at this priority; let me yield in the meantime."shouldYield()— "is the scheduler ready to give the browser a frame?" Called between fibers in the concurrent work loop.
Directory layout
packages/scheduler/
├── package.json
├── index.js # main entry → src/forks/Scheduler
├── index.native.js # React Native entry → SchedulerNative
├── unstable_mock.js # mock for tests
├── unstable_post_task.js # postTask-backed variant
└── src/
├── Scheduler.js # the production scheduler
├── SchedulerMock.js # the test mock
├── SchedulerNative.js # Native-platform variant
├── SchedulerPostTask.js # postTask-backed variant
├── SchedulerProfiling.js
├── SchedulerMinHeap.js # priority queue used by the main scheduler
├── SchedulerFeatureFlags.js
└── forks/ # per-build entry resolutionKey abstractions
| API | File | Description |
|---|---|---|
unstable_scheduleCallback(priority, callback, options?) |
packages/scheduler/src/Scheduler.js |
Enqueue work. Returns a task handle. |
unstable_cancelCallback(task) |
packages/scheduler/src/Scheduler.js |
Cancel a scheduled task. |
unstable_shouldYield() |
packages/scheduler/src/Scheduler.js |
True if the scheduler wants the caller to yield. |
unstable_runWithPriority(priority, fn) |
packages/scheduler/src/Scheduler.js |
Run fn synchronously at the given priority; nested calls inherit. |
unstable_now() |
varies | The scheduler's clock. Backed by performance.now() where available. |
| Priority levels | packages/scheduler/src/SchedulerPriorities.js |
ImmediatePriority, UserBlockingPriority, NormalPriority, LowPriority, IdlePriority. |
| Min-heap | packages/scheduler/src/SchedulerMinHeap.js |
Tiny self-contained priority queue. |
| Mock | packages/scheduler/src/SchedulerMock.js |
Exposes unstable_advanceTime, unstable_flushAll, unstable_flushExpired, unstable_flushNumberOfYields, log. Used by every reconciler test. |
How it works
graph TD Caller[react-reconciler] -->|unstable_scheduleCallback| Sched[Scheduler.js: push to min-heap] Sched -->|first task?| Channel[MessageChannel.port1.postMessage] Channel -->|onmessage in next tick| Loop[performWorkUntilDeadline] Loop -->|while work and now < deadline| Run[invoke callback] Run -->|callback returned a continuation| Loop Run -->|callback finished| Sched Loop -->|deadline reached| Channel
The main scheduler keeps two heaps: a task queue (work that's ready) and a timer queue (work delayed via delay). On each tick, expired timers are pushed into the task queue. Then the scheduler runs tasks, in priority order, until either:
- The task queue is empty.
- The deadline (typically 5 ms after the tick started) is reached. At that point
shouldYield()starts returning true, the scheduler yields to the browser, and anotherMessageChannelmessage reawakens it on the next macrotask.
The default frameYieldMs is 5 ms — short enough to keep input responsive, long enough that the overhead of yielding doesn't dominate.
MessageChannel vs postTask
Scheduler.js uses MessageChannel because:
setTimeout(fn, 0)is clamped to ≥ 4 ms in browsers.requestIdleCallbackis too low-priority and has unreliable timing.MessageChannel.port1.postMessageschedules a macrotask immediately and is supported everywhere.
SchedulerPostTask.js is a parallel implementation that uses the scheduler.postTask API where it exists. The reconciler can opt in via unstable_post_task. For now MessageChannel is still the default.
Native variant
React Native uses SchedulerNative.js, which targets RN's idle-callback bridge instead of MessageChannel. Same API, different driver.
Mock for tests
packages/scheduler/src/SchedulerMock.js is what scheduler/unstable_mock exports. Tests use it to step through scheduled work deterministically:
const Scheduler = require('scheduler/unstable_mock');
Scheduler.unstable_advanceTime(100);
Scheduler.unstable_flushAll();
expect(Scheduler).toHaveYielded(['A', 'B', 'C']);Scheduler.log(...) is the test pattern: components call Scheduler.log('rendered Foo') during render, and tests assert against Scheduler.unstable_clearLog() or toHaveYielded. The internal-test-utils package wraps these into waitForAll, waitForPaint, waitFor, etc.
Integration points
- react-reconciler is the only large consumer. It re-exports a curated subset via
packages/react-reconciler/src/Scheduler.js. react-dom-bindings/src/events/asks the scheduler forgetCurrentTimeandrunWithPriorityto map DOM events to the right priority.- External users — a small ecosystem of libraries (
react-tracked,redux-toolkit, etc.) importsunstable_scheduleCallbackdirectly, mostly for "yield between expensive computations" use cases. The team does not officially support this but tries not to break it.
Entry points for modification
- The default frame budget (
frameYieldMs) and continuation thresholds are top-of-file constants inScheduler.js. Changing them affects every concurrent React app. - The mock's
unstable_advanceTimesemantics are inSchedulerMock.js. Tests written against precise timings can be sensitive to changes here. - A new platform (Worklets, a new server runtime) gets a new
Scheduler<Platform>.jsthat exports the same API.
Related pages
- react-reconciler — the only first-party consumer.
- features/concurrent-rendering — how lanes and the scheduler cooperate.
- how-to-contribute/testing — the mock scheduler in tests.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.