scheduleCallback

scheduleCallback is the public entry point for adding work to Scheduler.

function scheduleCallback(
  priorityLevel: PriorityLevel,
  callback: Callback,
  options?: { delay: number },
): Task

It receives:

  • priorityLevel: how urgent the work is.
  • callback: the function Scheduler will eventually run.
  • options.delay: an optional delay before the task becomes eligible to run.

The function returns the created Task. React can later pass that task to cancelCallback.

Step 1: Compute startTime

The first step is to capture the current time and decide when the task is allowed to start:

var currentTime = getCurrentTime();

var startTime;
if (typeof options === "object" && options !== null) {
  var delay = options.delay;
  if (typeof delay === "number" && delay > 0) {
    startTime = currentTime + delay;
  } else {
    startTime = currentTime;
  }
} else {
  startTime = currentTime;
}

If delay is a positive number, the task is delayed and startTime is in the future. Otherwise, the task is ready immediately and startTime is the current time.

Step 2: Compute expirationTime

Next, Scheduler converts the priority into a timeout:

var timeout;
switch (priorityLevel) {
  case ImmediatePriority:
    timeout = -1;
    break;
  case UserBlockingPriority:
    timeout = userBlockingPriorityTimeout;
    break;
  case IdlePriority:
    timeout = maxSigned31BitInt;
    break;
  case LowPriority:
    timeout = lowPriorityTimeout;
    break;
  case NormalPriority:
  default:
    timeout = normalPriorityTimeout;
    break;
}

var expirationTime = startTime + timeout;

expirationTime is a deadline, not a timer. A user-blocking task with a 250ms timeout is not guaranteed to run exactly 250ms later. It can run earlier if Scheduler reaches it. The deadline tells Scheduler when the task has become overdue.

For delayed tasks, the deadline is based on the future startTime. That prevents a delayed task from expiring before it is even eligible to run.

Step 3: Create the Task

Scheduler then creates the task object:

var newTask: Task = {
  id: taskIdCounter++,
  callback,
  priorityLevel,
  startTime,
  expirationTime,
  sortIndex: -1,
};

sortIndex starts as -1 because Scheduler has not decided which queue the task belongs to yet. It will become either startTime or expirationTime.

Step 4: Put Delayed Tasks in timerQueue

If startTime > currentTime, the task is delayed:

if (startTime > currentTime) {
  newTask.sortIndex = startTime;
  push(timerQueue, newTask);

  if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
    if (isHostTimeoutScheduled) {
      cancelHostTimeout();
    } else {
      isHostTimeoutScheduled = true;
    }

    requestHostTimeout(handleTimeout, startTime - currentTime);
  }
}

Delayed tasks are ordered by startTime, so sortIndex is set to startTime.

The timeout scheduling condition is careful:

peek(taskQueue) === null && newTask === peek(timerQueue)

Scheduler schedules a host timeout only when there is no ready work and this new delayed task is the earliest timer. If another delayed task should start earlier, the existing timeout can stay. If this task is now the earliest one, Scheduler cancels the old timeout and schedules a new one for this task's start time.

Step 5: Put Ready Tasks in taskQueue

If the task is ready immediately, it goes into taskQueue:

else {
  newTask.sortIndex = expirationTime;
  push(taskQueue, newTask);

  if (!isHostCallbackScheduled && !isPerformingWork) {
    isHostCallbackScheduled = true;
    requestHostCallback();
  }
}

Ready tasks are ordered by expirationTime, so sortIndex is set to expirationTime.

After pushing the task, Scheduler requests a host callback unless one is already scheduled or Scheduler is currently inside the work loop. That is what starts the path toward performWorkUntilDeadline, flushWork, and finally workLoop.

Summary

scheduleCallback does not execute the callback directly. It only creates a task, puts it in the correct queue, and schedules the minimum host work needed to process it later.