Browser Work

Scheduler has two responsibilities:

  1. Keep React work ordered by priority.
  2. Avoid blocking the browser for too long while that work is running.

The second responsibility is what makes Scheduler more than a priority queue. If React performed every pending task in one long synchronous loop, the browser would not get enough time to respond to input, update layout, or paint the next frame.

What Counts as Browser Work?

Browser work includes everything the browser needs to do outside your currently running JavaScript:

  • handle user input
  • run event callbacks
  • calculate styles
  • perform layout
  • paint pixels to the screen
  • run animation frame callbacks

The important constraint is that JavaScript on the main thread runs to completion. While your JavaScript function is running, the browser cannot paint the next frame in the middle of that function.

There is no API like this:

processBrowserWork();

You cannot manually tell the browser, "paint right now," while the call stack is still occupied by your JavaScript. The browser gets its opportunity after the current task finishes and the event loop reaches the rendering step.

Why Not Use a Worker?

Workers are useful for isolated CPU-heavy calculations, but Scheduler's job is different. It coordinates when React work should continue on the main thread, especially when that work competes with user input and painting.

The Problem With One Long Task

Imagine taskQueue contains 100 ready tasks. A simple implementation could do this:

while (taskQueue.length > 0) {
  const task = taskQueue.shift();
  task.callback();
}

That approach is easy to understand, but it has a serious problem: if the loop takes 80ms, the browser is blocked for that entire 80ms. During that time, clicks may feel delayed, animations may stutter, and visual updates may not appear.

The browser is not ignoring the work. It is waiting for JavaScript to give control back.

Scheduler's Approach

Scheduler uses cooperative yielding. It runs work for a short amount of time, then stops before starting more non-expired work. If there is still work left, it schedules the work loop to continue in a later event cycle.

The flow looks like this:

  1. Run Scheduler work.
  2. Check whether the current time slice has been used.
  3. If there is still time, continue with the next task.
  4. If the time budget is used, yield back to the browser.
  5. Schedule the remaining work for another event cycle.

This does not make JavaScript parallel. It simply breaks one long block of work into smaller blocks, giving the browser opportunities between them.

The Time Budget

In this implementation, Scheduler uses a small time budget:

export const frameYieldMs = 5;

The relevant time-budget logic lives inside shouldYieldToHost:

function shouldYieldToHost(): boolean {
  const timeElapsed = getCurrentTime() - startTime;
  if (timeElapsed < frameInterval) {
    return false;
  }

  return true;
}

If Scheduler has been running for less than frameInterval, it keeps working. Once it reaches the budget, it yields before starting more non-expired work.

This check happens inside workLoop:

if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {
  break;
}

Notice the first condition: currentTask.expirationTime > currentTime. Scheduler yields only when the current task has not expired. If the task is already overdue, Scheduler treats it as urgent and continues processing it.

Continuing Later

Yielding only helps if Scheduler can continue the remaining work later. That continuation is scheduled through host APIs such as MessageChannel, setImmediate, or setTimeout.

Conceptually, it works like this:

performSomeWork();

if (hasMoreWork) {
  scheduleWorkForNextEventCycle();
}

When the browser gets control back, it can process input, paint, and run other pending work. Then, in a later event cycle, Scheduler resumes and processes more tasks.

Why This Matters for React

React work can be large. Updating a complex UI may require walking many Fiber nodes, computing changes, and preparing DOM updates. If all of that happens in one uninterrupted task, the page can feel frozen.

Scheduler gives React a way to make progress without monopolizing the main thread:

  • urgent work can run sooner
  • non-expired work can pause between chunks
  • the browser gets chances to paint and handle input
  • remaining work can resume later without losing its priority

This is the core idea behind Scheduler's cooperation with the browser: React does not make the browser work while JavaScript is running. It periodically stops running JavaScript so the browser has a chance to do its own work.