Skip to content

Deep Dive into React Scheduler: Trade-offs in Task Scheduler Choices

Tasks within a Frame

Typically, for the browser host environment, tasks within a single frame include event loop tick (executing one macrotask, then clearing all registered microtasks), browser rendering cycle APIs, and browser rendering tasks. The overall execution flow is as follows:

  1. Execute a Macrotask (Task): Select and execute the oldest, runnable macrotask from the macrotask queue. These macrotasks typically include callbacks for setTimeout, setInterval, MessageChannel, I/O operations, UI interaction events, etc.
  2. Execute all Microtasks (Microtask Checkpoint): Execute all microtasks that exist after the current macrotask has finished executing. If new microtasks are added during microtask execution, they continue to be executed until the microtask queue is empty. These microtasks typically include callbacks for Promise.resolve().then(fn), queueMicrotask(fn), MutationObserver, etc.
  3. Update the Rendering (On Demand):
    1. Execute all requestAnimationFrame (rAF) callbacks.
    2. Perform rendering steps such as style calculation, layout, compositing, and painting (if the browser determines rendering is necessary).
  4. Execute requestIdleCallback callbacks (On Demand and with Idle Time): If the current frame has idle time and there are callbacks in the requestIdleCallback queue, execute them.
Example Explanation
test.js
js
const channel = new MessageChannel();
channel.port1.onmessage = ({ data: { message } }) => {
  console.log('MessageChannel Callback  ', message);
};
const callback = () => {
  requestIdleCallback(() => {
    console.log('requestIdleCallback');
    Promise.resolve().then(res => {
      console.log('requestIdleCallback - Promise');
    });
    channel.port2.postMessage({ message: 'MessageChannel - 3' });
  });
  requestAnimationFrame(() => {
    channel.port2.postMessage({ message: 'MessageChannel - 2' });
    console.log('requestAnimationFrame');
    Promise.resolve().then(res => {
      console.log('requestAnimationFrame - Promise');
    });
  });
  Promise.resolve().then(res => {
    console.log('Promise');
    channel.port2.postMessage({ message: 'MessageChannel - 1' });
  });
};
callback();

The output of the above example is:

bash
Promise
requestAnimationFrame
requestAnimationFrame - Promise
MessageChannel Callback   MessageChannel - 1
MessageChannel Callback   MessageChannel - 2
requestIdleCallback
requestIdleCallback - Promise
MessageChannel Callback   MessageChannel - 3

Explanation:

Initial State: All task queues are empty.

  1. Tick 1: Initial Script Execution

    • Macrotask: The main thread starts executing the global script, which is the first macrotask.
      • The callback() function is called.
      • Promise.resolve().then(...) registers Microtask A.
      • requestIdleCallback(...) registers an Idle Macrotask to the Idle Task Queue.
      • requestAnimationFrame(...) registers an rAF Macrotask to the Animation Frame Callback Queue.
    • Macrotask End: Synchronous script code execution completes.
    • Microtask Phase: Clear the microtask queue.
      • Execute the callback for Microtask A, and Promise is logged to the console (✅).
      • Execute channel.port2.postMessage({ message: 'MessageChannel - 1' }), registering Macrotask M1 to the Standard Task Queue.
    • Tick 1 Execution End
      • Current Queue Status:
        • rAF Queue: [rAF callback]
        • Task Queue: [M1]
        • Idle Queue: [Idle callback]
  2. Tick 2: Animation Frame Callback

    • Macrotask: The event loop enters the next round. Based on priority, the browser prepares for a rendering update, so it takes the rAF Macrotask from the rAF Queue and executes it.
      • channel.port2.postMessage({ message: 'MessageChannel - 2' }) is called, registering Macrotask M2 to the standard task queue.
      • requestAnimationFrame is logged to the console (✅).
      • Promise.resolve().then(...) registers Microtask B.
    • Macrotask End: rAF callback execution completes.
    • Microtask Phase: Clear the microtask queue, execute the callback for Microtask B, and requestAnimationFrame - Promise is logged to the console (✅).
    • Tick 2 End
      • Current Queue Status:
        • Task Queue: [M1, M2]
        • Idle Queue: [Idle callback]
  3. Tick 3: First MessageChannel Message

    • Macrotask: The event loop takes Macrotask M1 from the standard task queue based on the FIFO principle and executes it.
      • channel.port1.onmessage callback is triggered, and MessageChannel Callback MessageChannel - 1 is logged to the console (✅).
    • Macrotask End: M1 callback execution completes.
    • Microtask Phase: The microtask queue is empty, skipped.
    • Tick 3 End
      • Current Queue Status:
        • Task Queue: [M2]
        • Idle Queue: [Idle callback]
  4. Tick 4: Second MessageChannel Message

    • Macrotask: The event loop continues to take Macrotask M2 from the standard task queue and executes it.
      • channel.port1.onmessage callback is triggered.
      • MessageChannel Callback MessageChannel - 2 is logged to the console (✅).
    • Macrotask End: M2 callback execution completes.
    • Microtask Phase: The microtask queue is empty, skipped.
    • Tick 4 End
      • Current Queue Status:
        • Task Queue: []
        • Idle Queue: [Idle callback]
  5. Tick 5: Idle State Callback (requestIdleCallback)

    • Macrotask: The event loop finds that high-priority queues are empty and the browser is in an idle state. Therefore, it takes the Idle Macrotask from the Idle Task Queue and executes it.
      • requestIdleCallback is logged to the console (✅).
      • Execute Promise.resolve().then(...) registering Microtask C.
      • Execute channel.port2.postMessage({ message: 'MessageChannel - 3' }), registering Macrotask M3 to the standard task queue.
    • Macrotask End: Idle callback execution completes.
    • Microtask Phase: Clear the microtask queue.
      • Execute the callback for Microtask C, and requestIdleCallback - Promise is logged to the console (✅).
    • Tick 5 End
      • Current Queue Status:
        • Task Queue: [M3]
        • Idle Queue: []
  6. Tick 6: Third MessageChannel Message

    • Macrotask: The event loop takes Macrotask M3 from the standard task queue and executes it.
      • channel.port1.onmessage callback is triggered.
      • MessageChannel Callback MessageChannel - 3 is logged to the console (✅).
    • Macrotask End: M3 callback execution completes.
    • Microtask Phase: The microtask queue is empty, skipped.
    • Tick 6 End
      • Current Queue Status: All different types of task queues are cleared, and task execution is complete.

Flow Summary

TickMacrotask TypeConsole Output
1Initial Script(None)
2requestAnimationFramerequestAnimationFrame requestAnimationFrame - Promise
3MessageChannel (First)MessageChannel Callback MessageChannel - 1
4MessageChannel (Second)MessageChannel Callback MessageChannel - 2
5requestIdleCallbackrequestIdleCallback requestIdleCallback - Promise
6MessageChannel (Third)MessageChannel Callback MessageChannel - 3

Next, requestAnimationFrame and requestIdleCallback will be collectively referred to as Environment Rendering Tasks (i.e., tasks scheduled by browser rendering cycle APIs). So, for browser host environment task types, they can be categorized into Macrotasks, Microtasks, and Environment Rendering Tasks. The following content will provide a specific feasibility analysis for these three types of tasks.

Feasibility Analysis

First, we need to determine whether to use macrotasks or microtasks.

Feasibility Analysis of Microtasks

React needs to have concurrent execution capabilities, which means that only a portion of tasks can be executed within a single event loop tick, with the remaining tasks distributed across subsequent event loop ticks.

Within a single event loop tick, all microtask queues are cleared. If scheduling employs microtasks, then the most basic thing that needs to be done is how to execute a limited number of microtasks within this event loop tick and then execute the remaining limited microtasks in subsequent event loop ticks.

Directly pushing multiple microtasks within a single event loop tick is impractical, as all these microtasks will be executed. This is synchronous scheduling, not concurrent scheduling. The meaning of concurrency is the ability to control which tasks to execute within a frame's tasks.

So, is there a way in the host environment to detect the start of an event loop tick, and at this moment, preemptively register microtasks to be executed, which seems to meet the requirements? Unfortunately, the host environment does not provide a hook to observe the start of an event loop tick, which means React cannot determine the timing of the next event loop tick through microtasks.

Since there isn't one, microtasks would need to be attached to environment APIs, triggering scheduling according to the execution timing of environment APIs. Let's analyze the environment APIs (requestAnimationFrame, requestIdleCallback).

Feasibility Analysis of Browser Rendering Tasks (rAF / rIC)

Test Case
test.js
js
let startTime = null;
let exCount = 0;
const handler = function (info) {
  return function (count) {
    console.log(
      `VI-LOG_${info}_${count} -- Time Overhead: `,
      new Date().getTime() - startTime
    );
  };
};
const requestIdleCallbackOnMessage = handler('requestIdleCallback');
const requestAnimationFrameOnMessage = handler('requestAnimationFrame');
const channel = new MessageChannel();
channel.port1.onmessage = function wrap({ data }) {
  const { count } = data;
  channelOnMessage(count);
};

const triggerMessageChannel = count => {
  channel.port2.postMessage({ count });
};
const callback = exCount => {
  startTime = new Date().getTime();
  requestIdleCallback(requestIdleCallbackOnMessage.bind(null, exCount));
  requestAnimationFrame(requestAnimationFrameOnMessage.bind(null, exCount));
};
let setInternalTime = new Date().getTime();
setInterval(() => {
  console.log(
    `------------------------------------ VI-LOG_Test_Start: ${++exCount} ------------------------------------`
  );
  console.log(
    `VI-LOG_setInterval_${exCount} -- Time Overhead: `,
    new Date().getTime() - setInternalTime
  );
  callback(exCount);
  setInternalTime = new Date().getTime();
}, 2000);
Prerequisites
- Baseline: chrome 137.0.7151.57 (Official Build) (arm64)
- 10th Iteration: Switched to another tab
- 113th Iteration: Refocused to current tab
  1. requestAnimationFrame Execution Behavior:

    • Foreground: The callback function executes before the browser's next repaint. This is typically synchronized with the display's refresh rate (e.g., 60Hz corresponds to about 16.7ms per frame). In the logs, requestAnimationFrame executes within a few milliseconds after the setInterval callback, as expected.
    • Background: When a tab is not active, the browser pauses the execution of requestAnimationFrame callbacks. All callbacks scheduled via requestAnimationFrame are queued until the tab becomes active again.
    • Refocus: Once the tab is reactivated, all requestAnimationFrame callbacks accumulated during the background period execute in a short burst, before new requestAnimationFrame requests are processed.
    • Feasibility Analysis
      • Background tab pause: When a tab goes into the background, requestAnimationFrame callbacks completely pause execution. If React's core work loop entirely relies on requestAnimationFrame, then all React updates (including state updates, side effect handling, etc.) will stop when the tab is in the background. This is unacceptable for applications that need to maintain some activity in the background (e.g., message notifications, data prefetching followed by state updates).
      • Usage: requestAnimationFrame scheduling is followed by browser rendering work, which is indeed suitable for React's commit phase synchronous tasks. However, React has not only rendering-related tasks but also a large number of side-effect tasks, and handling these side-effect tasks within requestAnimationFrame is not reasonable.
  2. requestIdleCallback Execution Behavior:

    • Foreground: The callback function is invoked when the browser's main thread is idle. This allows low-priority background tasks to be executed without affecting the user experience (e.g., animation fluidity).

    • Background: The behavior of requestIdleCallback differs from requestAnimationFrame. Even in the background, if the browser determines there are idle resources, requestIdleCallback callbacks may still be executed. However, the execution frequency and available timeRemaining() of background requestIdleCallback might be limited.

    • Refocus: Similar to requestAnimationFrame, if requestIdleCallback callbacks were scheduled in the background but failed to execute immediately for some reason (e.g., not enough idle time or browser policy), they might also get executed when the Tab is reactivated.

    • Feasibility Analysis

      • Background tab pause: requestIdleCallback still has a chance to execute in background tabs, although its frequency and reliability may decrease. This is better than the complete pause of requestAnimationFrame. The execution timing of requestIdleCallback is unstable; it only executes when the browser is idle and can be considered a low-priority task, easily monopolizing the main thread by other browser tasks, leading to starvation issues. This is unacceptable for user interactions that require timely responses (such as input field feedback) or high-priority updates.

      • Utilizing idle time: requestIdleCallback allows tasks to be executed when the browser's main thread is idle, which actually aligns with React's concurrent interruptible rendering. It also provides a deadline.timeRemaining() mechanism, which helps React determine how much remaining time is available in the current frame to perform work.

      • Compatibility issues: Safari v18.5 by default disables the requestIdleCallback feature, and IE and Opera Mini do not support it at all. User adoption is only 78.95%.

      • Hack behavior:

        requestIdleCallback can be configured with a timeout parameter to push low-priority tasks that time out to the host environment's macrotask queue. At the same time, tests show that when timeout > 0 (timeout = 0, background tasks accumulate and execute all at once upon refocus), the background execution trigger timing is also relatively stable.

        Therefore, if the scenario does not need to consider requestIdleCallback's compatibility and cross-platform issues, using requestIdleCallback(fn, { timeout: 1 }) can, to some extent, complete scheduling tasks.

        However, this is a hack behavior and goes against the original design intention of requestIdleCallback. The original design intention of requestIdleCallback is to schedule tasks as low-priority tasks, to be scheduled when the host environment is idle or when tasks are starving. The meaning of requestIdleCallback(fn, { timeout: 1 }) is to push tasks to the host environment's macrotask queue as soon as possible, waiting for the host environment to schedule them.

        React hopes to <mark>quickly respond and execute its highest priority internal tasks</mark>. Although scheduling can be done through hack behavior, the aggressiveness of timeout: 1 pushing macrotasks is not as high as setTimeout(fn, 0) and new MessageChannel(). Meanwhile, the hack method goes against the original intention of requestIdleCallback, and requestIdleCallback also has compatibility and cross-platform issues, so choosing requestIdleCallback is not a good choice.

In summary, requestAnimationFrame and requestIdleCallback are not suitable as React's general-purpose task schedulers. At this point in the discussion, neither host environment tasks nor microtasks are suitable.

Let's now analyze macrotasks. For React, a stable and efficient scheduling mechanism is needed, and macrotasks themselves are suitable as a scheduling mechanism. In each event loop tick, the browser picks a macrotask from the task queue to execute, and this process is generally stable and controllable.

Let's now analyze macrotasks.

Feasibility Analysis of Macrotasks

Test Case
test.js
js
let startTime = null;
let exCount = 0;
const handler = function (info) {
  return function (count) {
    console.log(
      `VI-LOG_${info}_${count} -- Time Overhead: `,
      new Date().getTime() - startTime
    );
  };
};
const channelOnMessage = handler('MessageChannel');
const setTimeoutPreOnMessage = handler('setTimeout pre');
const setTimeoutPostOnMessage = handler('setTimeout post');
const channel = new MessageChannel();
channel.port1.onmessage = function wrap({ data }) {
  const { count } = data;
  channelOnMessage(count);
};

const triggerMessageChannel = count => {
  channel.port2.postMessage({ count });
};
const callback = exCount => {
  startTime = new Date().getTime();
  setTimeout(setTimeoutPreOnMessage.bind(null, exCount), 0);
  triggerMessageChannel(exCount);
  setTimeout(setTimeoutPostOnMessage.bind(null, exCount), 0);
};
let setInternalTime = new Date().getTime();
setInterval(() => {
  console.log(
    `------------------------------------ VI-LOG_Test_Start: ${++exCount} ------------------------------------`
  );
  console.log(
    `VI-LOG_setInterval_${exCount} -- Time Overhead: `,
    new Date().getTime() - setInternalTime
  );
  callback(exCount);
  setInternalTime = new Date().getTime();
}, 2000);
Prerequisites
- Baseline: chrome 137.0.7151.57 (Official Build) (arm64)
- 10th Iteration: Switched to another tab
- 53rd Iteration: Refocused to current tab

Based on the chart, it can be analyzed that setTimeout and setInterval are unreliable in scenarios requiring continuous, precise, low-latency tasks in the background, as they are severely affected by browser throttling policies. In contrast, MessageChannel, as a macrotask, consistently maintains a time overhead of 0-2ms, whether the tab is in the foreground or background, performing as the most stable and efficient of the three. Therefore, MessageChannel is an ideal choice for reliable, high-performance task scheduling.

Note

When a tab goes into the background, browsers significantly throttle setTimeout(fn, 0), typically increasing its delay to about 1s. When in the background for about 12.4 minutes, the delay can increase to about 1 minute. In contrast, MessageChannel's message passing, even when the tab is in the background, exhibits relatively stable callback execution, although occasional delays (over 1s) may occur in non-active tab scenarios, it generally remains below 10ms. When the tab is reactivated, setTimeout's responsiveness recovers.

Now, let's analyze how React chooses its scheduling mechanism.

Selection Strategy

Simulating React's Scheduling Mechanism

Let's briefly simulate react's scheduling mechanism:

js
import { createSumTasks } from './src/task.mjs';
import { runConcurrent, runSync } from './src/reconciler.mjs';
import { SchedulerType } from './src/constants.mjs';

const tasks = createSumTasks(50);

const runTests = async () => {
  runSync(tasks);

  const runTest = schedulerType => {
    return new Promise(resolve => {
      console.log(`\n--- Testing [${schedulerType}] ---`);
      console.time(`${schedulerType} Total Time`);
      runConcurrent(tasks, schedulerType, () => {
        console.timeEnd(`${schedulerType} Total Time`);
        resolve();
      });
    });
  };

  await runTest(SchedulerType.MessageChannel);
  await runTest(SchedulerType.SetTimeout);

  if (typeof setImmediate !== 'undefined') {
    await runTest(SchedulerType.SetImmediate);
  } else {
    console.log(
      '\n--- Skipping SetImmediate test (not available in browser) ---'
    );
  }
};

runTests();
js
class TaskContainer {
  constructor() {
    this.tasks = [];
  }
  peek() {
    return this.tasks[0];
  }
  pop() {
    this.tasks.shift();
  }
  push(task) {
    this.tasks.push(task);
  }
  isEmpty() {
    return this.tasks.length === 0;
  }
}

const createTask = taskCount => {
  console.time(`Task ${taskCount + 1} Execution Time`);
  let sum = 0;
  for (let i = 0; i < 2000000; ++i) sum += i;
  console.timeEnd(`Task ${taskCount + 1} Execution Time`);
  return sum;
};

export const createSumTasks = count =>
  Array(count)
    .fill(1)
    .map((_, index) => createTask.bind(null, index));

export const taskContainer = new TaskContainer();
js
import { getScheduler, scheduleCallback } from './scheduler.mjs';
import { SchedulerType } from './constants.mjs';

const timeSlice = 5;
let pendingTask = null;

function workLoop(deadline, delay) {
  const scheduler = getScheduler();
  console.log(
    `\nStarting work loop with ${scheduler.type}, callback delay: ${delay}ms`
  );
  let shouldYield = false;

  while (pendingTask && !shouldYield) {
    pendingTask.handler[pendingTask.currentIndex]();
    pendingTask.currentIndex++;

    if (pendingTask.currentIndex >= pendingTask.handler.length) {
      pendingTask = null;
      console.log(`All tasks finished with ${scheduler.type}.`);
      if (scheduler.onComplete) scheduler.onComplete();
      return;
    }
    shouldYield = deadline.timeRemaining() < 1;
  }

  if (pendingTask) {
    console.log('Yielding...');
    scheduler.schedule(performWorkUntilDeadline);
  }
}

function performWorkUntilDeadline(delay) {
  const startTime = performance.now();
  const deadline = {
    timeRemaining: () =>
      Math.max(0, timeSlice - (performance.now() - startTime))
  };
  workLoop(deadline, delay);
}

export function runConcurrent(tasks, schedulerType, onComplete) {
  pendingTask = {
    handler: tasks,
    currentIndex: 0
  };

  scheduleCallback(({ callbackTime }) => {
    const delay = new Date().getTime() - callbackTime;
    performWorkUntilDeadline(delay);
  }, schedulerType);

  const scheduler = getScheduler();
  scheduler.onComplete = onComplete;
}

export function runSync(tasks) {
  console.log(`\n--- Testing [Synchronous] ---\n`);
  console.time('Synchronous Total Time');
  for (const task of tasks) {
    task();
  }
  console.timeEnd('Synchronous Total Time');
}
js
import { SchedulerType } from './constants.mjs';

let scheduler;
export const getScheduler = () => scheduler;

export const scheduleCallback = (
  callback,
  type = SchedulerType.MessageChannel
) => {
  const schedule = () => {
    switch (type) {
      case SchedulerType.MessageChannel: {
        const channel = new MessageChannel();
        channel.port1.onmessage = event => {
          callback(event.data);
          channel.port1.close();
        };
        channel.port2.postMessage({ callbackTime: new Date().getTime() });
        break;
      }
      case SchedulerType.SetTimeout: {
        setTimeout(
          callback.bind(null, { callbackTime: new Date().getTime() }),
          0
        );
        break;
      }
      case SchedulerType.SetImmediate: {
        setImmediate(
          callback.bind(null, { callbackTime: new Date().getTime() })
        );
        break;
      }
      default:
        break;
    }
  };

  scheduler = {
    type,
    schedule
  };

  schedule();
};
js
export const SchedulerType = {
  MessageChannel: 'MessageChannel',
  SetTimeout: 'SetTimeout',
  SetImmediate: 'SetImmediate'
};
Output Logs in a Browser Host Environment
bash
--- Testing [Synchronous] ---

 Task 1 Execution Time: 3.722900390625 ms
 Task 2 Execution Time: 2.032958984375 ms
 Task 3 Execution Time: 2.031005859375 ms
 Task 4 Execution Time: 2.037109375 ms
 Task 5 Execution Time: 2.046142578125 ms
 Task 6 Execution Time: 2.02587890625 ms
 Task 7 Execution Time: 2.134033203125 ms
 Task 8 Execution Time: 1.98486328125 ms
 Task 9 Execution Time: 1.9970703125 ms
 Task 10 Execution Time: 2.01806640625 ms
 Task 11 Execution Time: 2.010009765625 ms
 Task 12 Execution Time: 1.975830078125 ms
 Task 13 Execution Time: 1.9619140625 ms
 Task 14 Execution Time: 1.989013671875 ms
 Task 15 Execution Time: 2.037109375 ms
 Task 16 Execution Time: 1.97216796875 ms
 Task 17 Execution Time: 1.954833984375 ms
 Task 18 Execution Time: 1.916015625 ms
 Task 19 Execution Time: 1.902099609375 ms
 Task 20 Execution Time: 1.88916015625 ms
 Task 21 Execution Time: 1.89404296875 ms
 Task 22 Execution Time: 1.907958984375 ms
 Task 23 Execution Time: 1.931884765625 ms
 Task 24 Execution Time: 2.009033203125 ms
 Task 25 Execution Time: 1.889892578125 ms
 Task 26 Execution Time: 1.89599609375 ms
 Task 27 Execution Time: 1.9248046875 ms
 Task 28 Execution Time: 1.986083984375 ms
 Task 29 Execution Time: 1.976806640625 ms
 Task 30 Execution Time: 1.9228515625 ms
 Task 31 Execution Time: 1.89501953125 ms
 Task 32 Execution Time: 1.9970703125 ms
 Task 33 Execution Time: 1.989013671875 ms
 Task 34 Execution Time: 1.887939453125 ms
 Task 35 Execution Time: 1.887939453125 ms
 Task 36 Execution Time: 1.89794921875 ms
 Task 37 Execution Time: 1.93798828125 ms
 Task 38 Execution Time: 1.907958984375 ms
 Task 39 Execution Time: 1.899169921875 ms
 Task 40 Execution Time: 1.89306640625 ms
 Task 41 Execution Time: 2.011962890625 ms
 Task 42 Execution Time: 2.01220703125 ms
 Task 43 Execution Time: 2.010009765625 ms
 Task 44 Execution Time: 2.0029296875 ms
 Task 45 Execution Time: 1.90087890625 ms
 Task 46 Execution Time: 1.923828125 ms
 Task 47 Execution Time: 1.90283203125 ms
 Task 48 Execution Time: 1.89111328125 ms
 Task 49 Execution Time: 2.001953125 ms
 Task 50 Execution Time: 1.98291015625 ms
 Synchronous Total Time: 100.989990234375 ms

--- Testing [MessageChannel] ---

Starting work loop with MessageChannel, callback delay: 2ms
 Task 1 Execution Time: 1.947998046875 ms
 Task 2 Execution Time: 1.92822265625 ms
 Task 3 Execution Time: 1.93212890625 ms
 Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
 Task 4 Execution Time: 1.9599609375 ms
 Task 5 Execution Time: 2.025146484375 ms
 Task 6 Execution Time: 2.123046875 ms
 Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
 Task 7 Execution Time: 1.966064453125 ms
 Task 8 Execution Time: 1.983154296875 ms
 Yielding...

Starting work loop with MessageChannel, callback delay: 1ms
 Task 9 Execution Time: 1.97412109375 ms
 Task 10 Execution Time: 1.906005859375 ms
 Task 11 Execution Time: 1.929931640625 ms
 Yielding...

Starting work loop with MessageChannel, callback delay: 1ms
 Task 12 Execution Time: 1.900146484375 ms
 Task 13 Execution Time: 1.9208984375 ms
 Task 14 Execution Time: 2.090087890625 ms
 Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
 Task 15 Execution Time: 1.9140625 ms
 Task 16 Execution Time: 1.90283203125 ms
 Task 17 Execution Time: 1.890869140625 ms
 Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
 Task 18 Execution Time: 1.90478515625 ms
 Task 19 Execution Time: 1.890869140625 ms
 Task 20 Execution Time: 1.89111328125 ms
 Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
 Task 21 Execution Time: 1.906982421875 ms
 Task 22 Execution Time: 1.945068359375 ms
 Task 23 Execution Time: 1.9970703125 ms
 Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
 Task 24 Execution Time: 1.906982421875 ms
 Task 25 Execution Time: 1.971923828125 ms
 Task 26 Execution Time: 1.985107421875 ms
 Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
 Task 27 Execution Time: 2.0439453125 ms
 Task 28 Execution Time: 2.053955078125 ms
 Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
 Task 29 Execution Time: 2.033935546875 ms
 Task 30 Execution Time: 2.0400390625 ms
 Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
 Task 31 Execution Time: 3.156982421875 ms
 Task 32 Execution Time: 3.77294921875 ms
 Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
 Task 33 Execution Time: 2.02099609375 ms
 Task 34 Execution Time: 1.968994140625 ms
 Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
 Task 35 Execution Time: 1.958984375 ms
 Task 36 Execution Time: 2.0009765625 ms
 Task 37 Execution Time: 2.072998046875 ms
 Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
 Task 38 Execution Time: 2.02001953125 ms
 Task 39 Execution Time: 2.01513671875 ms
 Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
 Task 40 Execution Time: 2.019775390625 ms
 Task 41 Execution Time: 2.01904296875 ms
 Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
 Task 42 Execution Time: 2.02392578125 ms
 Task 43 Execution Time: 2.024169921875 ms
 Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
 Task 44 Execution Time: 2.016845703125 ms
 Task 45 Execution Time: 2.0849609375 ms
 Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
 Task 46 Execution Time: 1.9599609375 ms
 Task 47 Execution Time: 1.948974609375 ms
 Task 48 Execution Time: 1.91796875 ms
 Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
 Task 49 Execution Time: 1.906982421875 ms
 Task 50 Execution Time: 1.88916015625 ms
 All tasks finished with MessageChannel.
 MessageChannel Total Time: 108.123046875 ms

--- Testing [SetTimeout] ---

Starting work loop with SetTimeout, callback delay: 0ms
 Task 1 Execution Time: 1.93896484375 ms
 Task 2 Execution Time: 1.89013671875 ms
 Task 3 Execution Time: 1.998046875 ms
 Yielding...

Starting work loop with SetTimeout, callback delay: 0ms
 Task 4 Execution Time: 1.9169921875 ms
 Task 5 Execution Time: 1.9208984375 ms
 Task 6 Execution Time: 1.891845703125 ms
 Yielding...

Starting work loop with SetTimeout, callback delay: 0ms
 Task 7 Execution Time: 1.89892578125 ms
 Task 8 Execution Time: 1.928955078125 ms
 Task 9 Execution Time: 1.885986328125 ms
 Yielding...

Starting work loop with SetTimeout, callback delay: 0ms
 Task 10 Execution Time: 1.912109375 ms
 Task 11 Execution Time: 1.964111328125 ms
 Task 12 Execution Time: 2.014892578125 ms
 Yielding...

Starting work loop with SetTimeout, callback delay: 0ms
 Task 13 Execution Time: 1.944091796875 ms
 Task 14 Execution Time: 1.964111328125 ms
 Task 15 Execution Time: 1.989013671875 ms
 Yielding...

Starting work loop with SetTimeout, callback delay: 1ms
 Task 16 Execution Time: 1.93115234375 ms
 Task 17 Execution Time: 1.89111328125 ms
 Task 18 Execution Time: 1.902099609375 ms
 Yielding...

Starting work loop with SetTimeout, callback delay: 5ms
 Task 19 Execution Time: 1.89990234375 ms
 Task 20 Execution Time: 1.884033203125 ms
 Task 21 Execution Time: 1.89306640625 ms
 Yielding...

Starting work loop with SetTimeout, callback delay: 4ms
 Task 22 Execution Time: 2.032958984375 ms
 Task 23 Execution Time: 1.953125 ms
 Task 24 Execution Time: 1.962890625 ms
 Yielding...

Starting work loop with SetTimeout, callback delay: 5ms
 Task 25 Execution Time: 1.949951171875 ms
 Task 26 Execution Time: 2.08203125 ms
 Yielding...

Starting work loop with SetTimeout, callback delay: 4ms
 Task 27 Execution Time: 2.31982421875 ms
 Task 28 Execution Time: 2.2958984375 ms
 Yielding...

Starting work loop with SetTimeout, callback delay: 5ms
 Task 29 Execution Time: 2.325927734375 ms
 Task 30 Execution Time: 2.191162109375 ms
 Yielding...

Starting work loop with SetTimeout, callback delay: 5ms
 Task 31 Execution Time: 2.208984375 ms
 Task 32 Execution Time: 2.177978515625 ms
 Yielding...

Starting work loop with SetTimeout, callback delay: 5ms
 Task 33 Execution Time: 2.177001953125 ms
 Task 34 Execution Time: 2.201904296875 ms
 Yielding...

Starting work loop with SetTimeout, callback delay: 5ms
 Task 35 Execution Time: 2.051025390625 ms
 Task 36 Execution Time: 2.031982421875 ms
 Yielding...

Starting work loop with SetTimeout, callback delay: 4ms
 Task 37 Execution Time: 2.202880859375 ms
 Task 38 Execution Time: 2.169189453125 ms
 Yielding...

Starting work loop with SetTimeout, callback delay: 4ms
 Task 39 Execution Time: 2.177978515625 ms
 Task 40 Execution Time: 2.169921875 ms
 Yielding...

Starting work loop with SetTimeout, callback delay: 4ms
 Task 41 Execution Time: 2.280029296875 ms
 Task 42 Execution Time: 2.18798828125 ms
 Yielding...

Starting work loop with SetTimeout, callback delay: 5ms
 Task 43 Execution Time: 2.175048828125 ms
 Task 44 Execution Time: 2.051025390625 ms
 Yielding...

Starting work loop with SetTimeout, callback delay: 4ms
 Task 45 Execution Time: 2.02001953125 ms
 Task 46 Execution Time: 2.01611328125 ms
 Yielding...

Starting work loop with SetTimeout, callback delay: 5ms
 Task 47 Execution Time: 2.033935546875 ms
 Task 48 Execution Time: 2.011962890625 ms
 Yielding...

Starting work loop with SetTimeout, callback delay: 5ms
 Task 49 Execution Time: 2.18212890625 ms
 Task 50 Execution Time: 2.128173828125 ms
 All tasks finished with SetTimeout.
 SetTimeout Total Time: 174.214111328125 ms

Output Logs in a Node.js Host Environment
bash
--- Testing [Synchronous] ---

Task 1 Execution Time: 25.098ms
Task 2 Execution Time: 8.421ms
Task 3 Execution Time: 2.149ms
Task 4 Execution Time: 8.976ms
Task 5 Execution Time: 8.824ms
Task 6 Execution Time: 10.105ms
Task 7 Execution Time: 9.672ms
Task 8 Execution Time: 8.75ms
Task 9 Execution Time: 9.937ms
Task 10 Execution Time: 9.788ms
Task 11 Execution Time: 8.357ms
Task 12 Execution Time: 7.754ms
Task 13 Execution Time: 9.477ms
Task 14 Execution Time: 8.105ms
Task 15 Execution Time: 8.901ms
Task 16 Execution Time: 8.298ms
Task 17 Execution Time: 14.061ms
Task 18 Execution Time: 8.366ms
Task 19 Execution Time: 7.509ms
Task 20 Execution Time: 7.745ms
Task 21 Execution Time: 8.298ms
Task 22 Execution Time: 7.858ms
Task 23 Execution Time: 7.284ms
Task 24 Execution Time: 7.919ms
Task 25 Execution Time: 7.282ms
Task 26 Execution Time: 7.33ms
Task 27 Execution Time: 7.639ms
Task 28 Execution Time: 7.228ms
Task 29 Execution Time: 7.558ms
Task 30 Execution Time: 7.242ms
Task 31 Execution Time: 7.658ms
Task 32 Execution Time: 7.235ms
Task 33 Execution Time: 7.744ms
Task 34 Execution Time: 7.721ms
Task 35 Execution Time: 10.821ms
Task 36 Execution Time: 7.562ms
Task 37 Execution Time: 9.365ms
Task 38 Execution Time: 7.5ms
Task 39 Execution Time: 7.602ms
Task 40 Execution Time: 7.309ms
Task 41 Execution Time: 7.983ms
Task 42 Execution Time: 7.388ms
Task 43 Execution Time: 7.558ms
Task 44 Execution Time: 7.704ms
Task 45 Execution Time: 7.191ms
Task 46 Execution Time: 7.644ms
Task 47 Execution Time: 7.99ms
Task 48 Execution Time: 8.627ms
Task 49 Execution Time: 7.243ms
Task 50 Execution Time: 8.281ms
Sync Total Time: 432.438ms

--- Testing [MessageChannel] ---

Starting work loop with MessageChannel, callback delay: 28ms
Task 1 Execution Time: 7.841ms
Yielding...

Starting work loop with MessageChannel, callback delay: 1ms
Task 2 Execution Time: 8.266ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 3 Execution Time: 7.381ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 4 Execution Time: 7.662ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 5 Execution Time: 7.523ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 6 Execution Time: 8.608ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 7 Execution Time: 8.118ms
Yielding...

Starting work loop with MessageChannel, callback delay: 1ms
Task 8 Execution Time: 8.009ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 9 Execution Time: 7.349ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 10 Execution Time: 8.054ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 11 Execution Time: 7.714ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 12 Execution Time: 7.868ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 13 Execution Time: 7.411ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 14 Execution Time: 7.637ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 15 Execution Time: 7.501ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 16 Execution Time: 7.755ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 17 Execution Time: 7.54ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 18 Execution Time: 7.451ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 19 Execution Time: 8.255ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 20 Execution Time: 8.167ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 21 Execution Time: 7.676ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 22 Execution Time: 7.389ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 23 Execution Time: 7.835ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 24 Execution Time: 7.381ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 25 Execution Time: 7.723ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 26 Execution Time: 7.325ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 27 Execution Time: 7.926ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 28 Execution Time: 7.694ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 29 Execution Time: 7.74ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 30 Execution Time: 7.509ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 31 Execution Time: 8.088ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 32 Execution Time: 8.238ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 33 Execution Time: 10.393ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 34 Execution Time: 55.186ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 35 Execution Time: 9.902ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 36 Execution Time: 10.968ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 37 Execution Time: 7.874ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 38 Execution Time: 9.751ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 39 Execution Time: 8.158ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 40 Execution Time: 8.687ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 41 Execution Time: 10.244ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 42 Execution Time: 17.302ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 43 Execution Time: 14.435ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 44 Execution Time: 93.865ms
Yielding...

Starting work loop with MessageChannel, callback delay: 1ms
Task 45 Execution Time: 29.792ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 46 Execution Time: 13.411ms
Yielding...

Starting work loop with MessageChannel, callback delay: 1ms
Task 47 Execution Time: 28.059ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 48 Execution Time: 12.793ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 49 Execution Time: 10.047ms
Yielding...

Starting work loop with MessageChannel, callback delay: 0ms
Task 50 Execution Time: 12.102ms
All tasks finished with MessageChannel.
MessageChannel Total Time: 661.233ms

--- Testing [SetTimeout] ---

Starting work loop with SetTimeout, callback delay: 1ms
Task 1 Execution Time: 13.236ms
Yielding...

Starting work loop with SetTimeout, callback delay: 2ms
Task 2 Execution Time: 10.056ms
Yielding...

Starting work loop with SetTimeout, callback delay: 1ms
Task 3 Execution Time: 11.915ms
Yielding...

Starting work loop with SetTimeout, callback delay: 2ms
Task 4 Execution Time: 11.289ms
Yielding...

Starting work loop with SetTimeout, callback delay: 2ms
Task 5 Execution Time: 9.037ms
Yielding...

Starting work loop with SetTimeout, callback delay: 1ms
Task 6 Execution Time: 11.884ms
Yielding...

Starting work loop with SetTimeout, callback delay: 1ms
Task 7 Execution Time: 11.763ms
Yielding...

Starting work loop with SetTimeout, callback delay: 2ms
Task 8 Execution Time: 11.801ms
Yielding...

Starting work loop with SetTimeout, callback delay: 1ms
Task 9 Execution Time: 10.262ms
Yielding...

Starting work loop with SetTimeout, callback delay: 1ms
Task 10 Execution Time: 8.771ms
Yielding...

Starting work loop with SetTimeout, callback delay: 1ms
Task 11 Execution Time: 8.516ms
Yielding...

Starting work loop with SetTimeout, callback delay: 1ms
Task 12 Execution Time: 13.238ms
Yielding...

Starting work loop with SetTimeout, callback delay: 1ms
Task 13 Execution Time: 7.923ms
Yielding...

Starting work loop with SetTimeout, callback delay: 2ms
Task 14 Execution Time: 8.7ms
Yielding...

Starting work loop with SetTimeout, callback delay: 2ms
Task 15 Execution Time: 7.922ms
Yielding...

Starting work loop with SetTimeout, callback delay: 1ms
Task 16 Execution Time: 8.342ms
Yielding...

Starting work loop with SetTimeout, callback delay: 2ms
Task 17 Execution Time: 7.938ms
Yielding...

Starting work loop with SetTimeout, callback delay: 0ms
Task 18 Execution Time: 8.889ms
Yielding...

Starting work loop with SetTimeout, callback delay: 1ms
Task 19 Execution Time: 7.805ms
Yielding...

Starting work loop with SetTimeout, callback delay: 1ms
Task 20 Execution Time: 7.868ms
Yielding...

Starting work loop with SetTimeout, callback delay: 1ms
Task 21 Execution Time: 8.572ms
Yielding...

Starting work loop with SetTimeout, callback delay: 1ms
Task 22 Execution Time: 9.975ms
Yielding...

Starting work loop with SetTimeout, callback delay: 2ms
Task 23 Execution Time: 9.685ms
Yielding...

Starting work loop with SetTimeout, callback delay: 1ms
Task 24 Execution Time: 17.216ms
Yielding...

Starting work loop with SetTimeout, callback delay: 2ms
Task 25 Execution Time: 7.561ms
Yielding...

Starting work loop with SetTimeout, callback delay: 1ms
Task 26 Execution Time: 8.147ms
Yielding...

Starting work loop with SetTimeout, callback delay: 1ms
Task 27 Execution Time: 8.373ms
Yielding...

Starting work loop with SetTimeout, callback delay: 2ms
Task 28 Execution Time: 7.816ms
Yielding...

Starting work loop with SetTimeout, callback delay: 1ms
Task 29 Execution Time: 7.72ms
Yielding...

Starting work loop with SetTimeout, callback delay: 0ms
Task 30 Execution Time: 7.241ms
Yielding...

Starting work loop with SetTimeout, callback delay: 1ms
Task 31 Execution Time: 7.554ms
Yielding...

Starting work loop with SetTimeout, callback delay: 1ms
Task 32 Execution Time: 8.101ms
Yielding...

Starting work loop with SetTimeout, callback delay: 2ms
Task 33 Execution Time: 7.386ms
Yielding...

Starting work loop with SetTimeout, callback delay: 1ms
Task 34 Execution Time: 7.318ms
Yielding...

Starting work loop with SetTimeout, callback delay: 1ms
Task 35 Execution Time: 7.319ms
Yielding...

Starting work loop with SetTimeout, callback delay: 1ms
Task 36 Execution Time: 7.363ms
Yielding...

Starting work loop with SetTimeout, callback delay: 1ms
Task 37 Execution Time: 7.359ms
Yielding...

Starting work loop with SetTimeout, callback delay: 1ms
Task 38 Execution Time: 7.599ms
Yielding...

Starting work loop with SetTimeout, callback delay: 2ms
Task 39 Execution Time: 7.448ms
Yielding...

Starting work loop with SetTimeout, callback delay: 1ms
Task 40 Execution Time: 7.664ms
Yielding...

Starting work loop with SetTimeout, callback delay: 1ms
Task 41 Execution Time: 7.597ms
Yielding...

Starting work loop with SetTimeout, callback delay: 1ms
Task 42 Execution Time: 8.319ms
Yielding...

Starting work loop with SetTimeout, callback delay: 0ms
Task 43 Execution Time: 8.281ms
Yielding...

Starting work loop with SetTimeout, callback delay: 1ms
Task 44 Execution Time: 8.249ms
Yielding...

Starting work loop with SetTimeout, callback delay: 1ms
Task 45 Execution Time: 7.494ms
Yielding...

Starting work loop with SetTimeout, callback delay: 1ms
Task 46 Execution Time: 7.404ms
Yielding...

Starting work loop with SetTimeout, callback delay: 1ms
Task 47 Execution Time: 7.229ms
Yielding...

Starting work loop with SetTimeout, callback delay: 1ms
Task 48 Execution Time: 7.44ms
Yielding...

Starting work loop with SetTimeout, callback delay: 2ms
Task 49 Execution Time: 7.521ms
Yielding...

Starting work loop with SetTimeout, callback delay: 2ms
Task 50 Execution Time: 7.396ms
All tasks finished with SetTimeout.
SetTimeout Total Time: 508.129ms

--- Testing [SetImmediate] ---

Starting work loop with SetImmediate, callback delay: 0ms
Task 1 Execution Time: 7.276ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 2 Execution Time: 7.467ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 3 Execution Time: 7.173ms
Yielding...

Starting work loop with SetImmediate, callback delay: 1ms
Task 4 Execution Time: 7.425ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 5 Execution Time: 7.31ms
Yielding...

Starting work loop with SetImmediate, callback delay: 1ms
Task 6 Execution Time: 8.497ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 7 Execution Time: 7.269ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 8 Execution Time: 7.528ms
Yielding...

Starting work loop with SetImmediate, callback delay: 1ms
Task 9 Execution Time: 7.353ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 10 Execution Time: 7.614ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 11 Execution Time: 7.481ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 12 Execution Time: 7.268ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 13 Execution Time: 7.728ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 14 Execution Time: 7.278ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 15 Execution Time: 7.477ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 16 Execution Time: 7.212ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 17 Execution Time: 7.504ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 18 Execution Time: 7.242ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 19 Execution Time: 8.966ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 20 Execution Time: 7.295ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 21 Execution Time: 7.546ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 22 Execution Time: 7.217ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 23 Execution Time: 7.348ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 24 Execution Time: 7.317ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 25 Execution Time: 7.22ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 26 Execution Time: 7.755ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 27 Execution Time: 7.24ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 28 Execution Time: 7.55ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 29 Execution Time: 7.29ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 30 Execution Time: 7.931ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 31 Execution Time: 7.306ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 32 Execution Time: 9.013ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 33 Execution Time: 7.68ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 34 Execution Time: 8.519ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 35 Execution Time: 7.398ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 36 Execution Time: 7.54ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 37 Execution Time: 7.276ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 38 Execution Time: 8.048ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 39 Execution Time: 7.404ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 40 Execution Time: 7.46ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 41 Execution Time: 7.311ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 42 Execution Time: 7.18ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 43 Execution Time: 7.529ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 44 Execution Time: 7.226ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 45 Execution Time: 7.434ms
Yielding...

Starting work loop with SetImmediate, callback delay: 1ms
Task 46 Execution Time: 8.103ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 47 Execution Time: 7.366ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 48 Execution Time: 7.148ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 49 Execution Time: 7.454ms
Yielding...

Starting work loop with SetImmediate, callback delay: 0ms
Task 50 Execution Time: 7.28ms
All tasks finished with SetImmediate.
SetImmediate Total Time: 384.324ms

Analyzing the performance of the simulated scheduler in node.js and browser environments:

  1. setImmediate

    The time from scheduling to triggering a task is almost always 0ms, demonstrating stability. This shows that setImmediate, as its name suggests, executes immediately and is semantic.

  2. setTimeout

    In both node.js and browser environments, the execution time of setTimeout fluctuates. When setTimeout is configured to 0ms and the tab is in focus, it still exhibits a delayed execution characteristic, with the delay stabilizing around 4ms, especially in deeply nested calls. In other words, in client-side scenarios (client-side rendering or hydration), if setTimeout(..., 0) is used for react's scheduling, an additional 4ms delay cost is incurred when there are more than 5 tasks, which does not align with react's efficient scheduling strategy.

    In unfocused scenarios, setTimeout also shows a phenomenon of low-frequency delayed scheduling, affecting scheduling efficiency in background scenarios.

  3. MessageChannel

    Under the MessageChannel scheduler, the execution time is almost always 0ms, indicating stability. However, in the node.js environment, the initial execution time of MessageChannel is relatively long, and the process does not exit when there are no more tasks, requiring manual closing of the MessageChannel's port to terminate the process.

Although the above schedulers perform differently in node.js and browser environments, they can all serve as schedulers for react, with performance variations in certain scenarios.

From the analysis above, we can establish a simple priority for scheduler selection:

setImmediate (node.js) > MessageChannel (browser) > setTimeout (browser)

So how does react choose its scheduler?

React's Selection Strategy

react also uses macrotasks for task scheduling in its scheduler package. The code is as follows:

scheduler.js
js
// Capture local references to native APIs, in case a polyfill overrides them.
const localSetTimeout =
  typeof setTimeout === 'function' ? setTimeout : null;
const localSetImmediate =
  typeof setImmediate !== 'undefined' ? setImmediate : null; // IE and Node.js + jsdom
const performWorkUntilDeadline = () => {
  if (typeof localSetImmediate === 'function') {
    // Node.js and old IE.
    // There's a few reasons for why we prefer setImmediate.
    //
    // Unlike MessageChannel, it doesn't prevent a Node.js process from exiting.
    // (Even though this is a DOM fork of the Scheduler, you could get here
    // with a mix of Node.js 15+, which has a MessageChannel, and jsdom.)
    // https://github.com/facebook/react/issues/20756
    //
    // But also, it runs earlier which is the semantic we want.
    // If other browsers ever implement it, it's better to use it.
    // Although both of these would be inferior to native scheduling.
    schedulePerformWorkUntilDeadline = () => {
      localSetImmediate(performWorkUntilDeadline);
    };
  } else if (typeof MessageChannel !== 'undefined') {
    // DOM and Worker environments.
    // We prefer MessageChannel because of the 4ms setTimeout clamping.
    const channel = new MessageChannel();
    const port = channel.port2;
    channel.port1.onmessage = performWorkUntilDeadline;
    schedulePerformWorkUntilDeadline = () => {
      port.postMessage(null);
    };
  } else {
    // We should only fallback here in non-browser environments.
    schedulePerformWorkUntilDeadline = () => {
      // $FlowFixMe[not-a-function] nullable value
      localSetTimeout(performWorkUntilDeadline, 0);
    };
  }
};

From the code comments and logic, it's clear that react's selection priority is: setImmediate > MessageChannel > setTimeout, which is consistent with the priority order we analyzed earlier.

Limitations of Generic API Scheduling

Inability to Distinguish Task Priorities

Problem: Generic APIs (MessageChannel, setTimeout) can only schedule all tasks with a single, identical priority. Each time React's scheduler needs to decide which task to push, its internal min-heap implementation selects the highest or immediate priority task, simulating priority scheduling in user space. The task is then handed over to a generic API for execution. However, the generic API cannot distinguish task priorities and can only schedule all tasks at the same priority level. This means that tasks executed by MessageChannel(task_A) and MessageChannel(task_B) have the same priority.

Solution: postTask allows specifying different priorities for tasks (user-blocking, user-visible, background). Essentially, the browser acts as a global scheduler, processing tasks at appropriate times based on the priority provided by the scheduler. In other words, the browser can perform true preemptive scheduling from a global perspective, whereas the React scheduler's view is limited to its own internal simulated task queue in the min-heap. This is the most fundamental difference between the two.

Inefficient Task Interruption and Resumption

Problem: Currently, react implements time-slicing using a 5ms threshold. During each task execution, it checks if the threshold has been exceeded. If so, it voluntarily yields control, stops executing subsequent react tasks, and hands over main thread control to the host environment. The remaining tasks are left for the next event loop tick.

Scheduler.js
ts
const frameYieldMs = 5;
const frameInterval = frameYieldMs;
function shouldYieldToHost(): boolean {
  if (!enableAlwaysYieldScheduler && enableRequestPaint && needsPaint) {
    // Yield now.
    return true;
  }
  const timeElapsed = getCurrentTime() - startTime;
  if (timeElapsed < frameInterval) {
    // The main thread has only been blocked for a really short amount of time;
    // smaller than a single frame. Don't yield yet.
    return false;
  }
  // Yield now.
  return true;
}
const performWorkUntilDeadline = () => {
  if (enableRequestPaint) {
    needsPaint = false;
  }
  if (isMessageLoopRunning) {
    const currentTime = getCurrentTime();
    // Keep track of the start time so we can measure how long the main thread
    // has been blocked.
    startTime = currentTime;

    // If a scheduler task throws, exit the current browser task so the
    // error can be observed.
    //
    // Intentionally not using a try-catch, since that makes some debugging
    // techniques harder. Instead, if `flushWork` errors, then `hasMoreWork` will
    // remain true, and we'll continue the work loop.
    let hasMoreWork = true;
    try {
      hasMoreWork = flushWork(currentTime);
    } finally {
      if (hasMoreWork) {
        // If there's more work, schedule the next message event at the end
        // of the preceding one.
        schedulePerformWorkUntilDeadline();
      } else {
        isMessageLoopRunning = false;
      }
    }
  }
};

The 5ms value is the most suitable time determined by the react team through experiments. While it's a reasonable duration in typical scenarios, the host environment's scheduling is complex. The available time for task execution within a frame is dynamic. If there are no higher-priority tasks waiting, yielding the main thread and resuming execution in another event loop tick is not very efficient.

Solution: postTask allows the browser to manage task priorities. Within a task, calling await scheduler.yield() lets the browser decide whether to yield the main thread based on the presence of other high-priority tasks. The browser saves the task's context and can immediately resume its execution context after handling the high-priority tasks, completing the original task more efficiently.

ts
const task = async () => {
  // ...
};
const sumTasks = Array(1000).fill(task);
const longTask = async () => {
  for (const task of sumTasks) {
    await task();
    await scheduler.yield();
  }
};
scheduler.postTask(longTask, { priority: 'background' });

When executing a long task, scheduler.yield() is used to let the browser decide if there are higher-priority tasks to process. If so, it yields the main thread. After the browser handles the high-priority tasks, it immediately resumes the original task's execution, continuing with the subsequent tasks in the for ... of loop.

scheduler.postTask and scheduler.yield() refine the logic of shouldYieldToHost and enable more efficient internal task resumption in react. They shift the decision-maker from React's prediction to the browser's omniscient view. The browser truly knows if there are higher-priority tasks that need processing at any given moment, thus making decisions that align with the actual situation.

Task Cancellation

As we've seen, react scheduler currently uses setImmediate, MessageChannel, and setTimeout as its task schedulers. These schedulers all share a common problem: they cannot cancel tasks. So how does react scheduler solve this problem?

Scheduler.js
ts
function unstable_cancelCallback(task: Task) {
  if (enableProfiling) {
    if (task.isQueued) {
      const currentTime = getCurrentTime();
      markTaskCanceled(task, currentTime);
      task.isQueued = false;
    }
  }

  // Null out the callback to indicate the task has been canceled. (Can't
  // remove from the queue because you can't remove arbitrary nodes from an
  // array based heap, only the first one.)
  task.callback = null;
}

unstable_cancelCallback is the internal function in react scheduler used to cancel tasks. It cancels a task by setting task.callback = null.

This affects both the timerQueue and the taskQueue. In the timerQueue:

Scheduler.js
ts
function advanceTimers(currentTime: number) {
  // Check for tasks that are no longer delayed and add them to the queue.
  let timer = peek(timerQueue);
  while (timer !== null) {
    if (timer.callback === null) {
      // Timer was cancelled.
      pop(timerQueue);
    } else if (timer.startTime <= currentTime) {
      // Timer fired. Transfer to the task queue.
      pop(timerQueue);
      timer.sortIndex = timer.expirationTime;
      push(taskQueue, timer);
      if (enableProfiling) {
        markTaskStart(timer, currentTime);
        timer.isQueued = true;
      }
    } else {
      // Remaining timers are pending.
      return;
    }
    timer = peek(timerQueue);
  }
}

When timer.callback === null, it indicates that the task has been canceled. pop(timerQueue) is called directly to remove the task, so it doesn't even get a chance to enter the taskQueue.

In the taskQueue:

Scheduler.js
ts
function workLoop(initialTime: number) {
  currentTask = peek(taskQueue);
  while (currentTask !== null) {
    const callback = currentTask.callback;
    if (typeof callback === 'function') {
      // ...
    } else {
      pop(taskQueue);
    }
  }
}

When typeof callback !== 'function', it means the task has been canceled, and pop(taskQueue) is called directly to remove it.

If a task yields control during execution and there are still other tasks to process, react scheduler will assign the subsequent work as a new task and initiate a new performWorkUntilDeadline scheduling task.

Scheduler.js
ts
function workLoop(initialTime: number) {
  currentTask = peek(taskQueue);
  while (currentTask !== null) {
    const callback = currentTask.callback;
    if (typeof callback === 'function') {
      currentTask.callback = null;
      currentPriorityLevel = currentTask.priorityLevel;
      const didUserCallbackTimeout =
        currentTask.expirationTime <= currentTime;
      const continuationCallback = callback(didUserCallbackTimeout);
      if (typeof continuationCallback === 'function') {
        currentTask.callback = continuationCallback;
        advanceTimers(currentTime);
        return true;
      } else {
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
        advanceTimers(currentTime);
      }
    }
  }
}

const performWorkUntilDeadline = () => {
  let hasMoreWork = true;
  try {
    hasMoreWork = flushWork(currentTime);
  } finally {
    if (hasMoreWork) {
      // If there's more work, schedule the next message event at the end
      // of the preceding one.
      schedulePerformWorkUntilDeadline();
    } else {
      isMessageLoopRunning = false;
    }
  }
}

react scheduler uses the presence of a function in task.callback as the criterion for whether a task should be executed. If task.callback is null, it means the task has been canceled and will not be executed. The task in task.callback is a one-off; task.callback is set to null before the task executes. If the task yields control during execution while there is still more work to be done, the task itself is not destroyed. Instead, the remaining work is assigned as a new task to task.callback, and a new round of the performWorkUntilDeadline scheduling task is initiated.

scheduler.postTask can be used in conjunction with TaskController to implement native task cancellation.

SchedulerPostTask.js
ts
const controller = new TaskController({ priority: postTaskPriority });
const postTaskOptions = {
  delay:
    typeof options === 'object' && options !== null ? options.delay : 0,
  signal: controller.signal
};
scheduler
  .postTask(
    runTask.bind(null, priorityLevel, postTaskPriority, node, callback),
    postTaskOptions
  )
  .catch(handleAbortError);

The callback task can be canceled by calling controller.abort().

SchedulerPostTask.js
ts
export function unstable_cancelCallback(node: CallbackNode) {
  const controller = node._controller;
  controller.abort();
}

react scheduler has already simulated the abort mechanism of TaskController internally. So, what are the advantages of handing over the task cancellation mechanism to the browser for management?

  1. Performance and Efficiency

    • Deeper Optimization: The browser can manage and abort tasks at a much lower level. When a task is aborted, the browser can genuinely cancel the underlying operations (like network requests, file I/O, etc.), thereby saving cpu, memory, and network resources.
    • Avoiding Unnecessary javascript Execution: The simulated mechanism in react scheduler essentially still runs at the javascript level. It decides whether to execute the next task fragment by checking a flag (task.callback === null). While efficient, this still requires scheduling and checks within the javascript engine. In contrast, the native AbortController can directly notify the browser kernel to stop the relevant activities, reducing unnecessary script execution and potential event loop congestion.
  2. Reliability in Complex Scenarios

    • Unified Signal Propagation: AbortSignal provides a clear and consistent way to propagate a cancellation signal. A single AbortSignal can be passed to multiple different asynchronous operations. When abort() is called, all associated operations receive the signal. This pattern is clear, easy to understand and maintain, and reduces the complexity of manually managing multiple cancellation flags.

    • Atomicity: The browser's cancellation signal propagation and state changes are atomic, handling concurrent scenarios more reliably. A simulated approach could lead to race conditions or inconsistent states in complex situations.

Note

The core value of AbortSignal lies in managing and optimizing host environment resources; it is a purely front-end/client-side mechanism.

In most scenarios, AbortSignal cannot completely prevent side effects that have already occurred. The front-end must handle the side effects of a rejected request by considering numerous edge cases. For example, when a fetch request is sent to the backend, AbortSignal cannot inform the server that the request has been aborted; the server will continue processing the request until it completes. Although the user could use a rapid communication channel to notify the server of the cancellation, this doesn't prevent the browser from consuming resources to download the response data, nor does it solve issues like the application layer needing to distinguish whether the received data is invalid, or how to handle data after the application is destroyed.

The essence of the AbortSignal mechanism is to notify the host environment that a task is invalid. The host environment will then assist in aborting the task while optimizing the impact of its side effects, thereby avoiding unnecessary cpu consumption and memory leaks.

Issues with the Scheduler API

  1. Poor Browser Compatibility

    scheduler.postTask:

    • Supported: Mainly in chromium-based browsers (chrome, edge, opera) since around version 94.
    • Not Supported: firefox support starts from the latest Nightly version 142. safari does not support it at all.

    scheduler.yield:

    • Supported: Mainly in chromium-based browsers (chrome, edge, opera) since around version 129.
    • Not Supported: firefox support starts from the latest Nightly version 142. safari does not support it at all.
  2. Insufficient Features

    No Starvation Handling Mechanism:

    react scheduler can set an expirationTime for a task. If a low-priority task is continuously preempted by higher-priority tasks and doesn't get to run, once it expires, react scheduler will promote it to the highest priority and execute it synchronously to prevent the task from "starving". The native scheduler API currently lacks this mechanism.

Contributors

Changelog

Discuss

Released under the CC BY-SA 4.0 License. (95ee605)