Open-Source Wikis

/

React

/

Packages

/

scheduler

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:

  1. scheduleCallback(priority, fn) — "run fn later at this priority; let me yield in the meantime."
  2. 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 resolution

Key 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 another MessageChannel message 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.
  • requestIdleCallback is too low-priority and has unreliable timing.
  • MessageChannel.port1.postMessage schedules 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 for getCurrentTime and runWithPriority to map DOM events to the right priority.
  • External users — a small ecosystem of libraries (react-tracked, redux-toolkit, etc.) imports unstable_scheduleCallback directly, 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 in Scheduler.js. Changing them affects every concurrent React app.
  • The mock's unstable_advanceTime semantics are in SchedulerMock.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>.js that exports the same API.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

scheduler – React wiki | Factory