⚛ The Course  /  Module 4 · Timing (The Scheduler) · 4.1
Module 4 · Timing (The Scheduler)

4.1 Why time-slicing

Drawing a large React tree is a lot of JavaScript. If React ran all of it in one unbroken burst, the page would freeze — clicks ignored, nothing repainting — until that burst finished. This lesson explains why React refuses to render everything at once, and instead keeps handing control back to the browser.

The browser has one main thread

The browser does almost everything on a single thread, called the main thread.

That one thread runs your JavaScript, responds to clicks and keystrokes, and paints pixels to the screen.

It can only do one of those at a time. While it is busy running a function, it cannot also handle a click or repaint the screen.

One long task freezes the page

Rendering is just JavaScript. React walks the Fiber tree, calls your components, and runs beginWork on each fiber.

For a big tree this takes real time. The Scheduler chapter's running example renders 10,000 items, which can take hundreds of milliseconds.

If React does all of that in one go, the main thread is busy the entire time. Nothing else can run: a click sits in a queue, and the screen stops updating. The page is frozen.

The fix: render a little, then step aside

So React breaks the long render into small slices.

It does a little work, then yields — it hands the main thread back to the browser.

The browser uses that gap to process a queued click and to repaint. Then React picks up where it left off and runs the next slice.

Here is the difference, drawn out — the same 10,000 items, one task versus many small ones:

Without message loop (blocking):
├──────────────────────────────────────────────────►  500ms
│ Render 10,000 items — main thread BLOCKED
└────────────────────────────────────────────────────

With message loop (non-blocking):
├──5ms──┬─free─┬──5ms──┬─free─┬──5ms──┬─free─┬──► ...
│Items  │      │Items  │      │Items  │      │
│0–100  │Event │101–200│Event │201–300│Event │
│       │queue │       │queue │       │queue │
└───────┴──────┴───────┴──────┴───────┴──────┴──► ...

This stop-and-go is called time-slicing. Notice the total rendering work does not shrink — it is the same items either way. What changes is that the browser gets a turn between slices, so the page stays alive instead of locking up.

The real code

React keeps two versions of its inner rendering loop. The only difference between them is whether the loop is allowed to stop early and hand control back.

// Concurrent mode — checked before each fiber
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

// Sync mode — no yield check at all
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

workLoopConcurrent checks !shouldYield() before each fiber. Once React has used up its small time budget, shouldYield() becomes true, the loop exits, and the main thread is freed — that is time-slicing.

workLoopSync has no such check. It runs until the whole tree is done, never pausing. That second loop is the one that can freeze the page, so React only uses it for work that must not be interrupted.

Show the full source — what shouldYield() measures
// SchedulerFeatureFlags.js
export const frameYieldMs = 5;       // Default yield interval
export const continuousYieldMs = 50; // For continuous input (mousemove)
export const maxYieldMs = 300;       // Absolute maximum before forced yield

// shouldYieldToHost — shipped in React 18.3.1 (simple time-based check;
// the isInputPending branch is gated behind enableIsInputPending = false)
function shouldYieldToHost() {
  const timeElapsed = getCurrentTime() - startTime;
  if (timeElapsed < frameInterval) {  // frameInterval = 5ms
    return false;
  }
  return true;
}

Underneath, shouldYield() is just “has 5 ms passed since this slice started?”. The exact budget, and how the pause is wired up so React can resume, are the subject of the next two lessons (4.2 and 4.3).