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:
- 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. - 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. - Update the Rendering (On Demand):
- Execute all
requestAnimationFrame (rAF)
callbacks. - Perform rendering steps such as style calculation, layout, compositing, and painting (if the browser determines rendering is necessary).
- Execute all
- Execute
requestIdleCallback
callbacks (On Demand and with Idle Time): If the current frame has idle time and there are callbacks in therequestIdleCallback
queue, execute them.
Example Explanation
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:
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.
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.
- The
- 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.
- Execute the callback for Microtask A, and
Tick 1
Execution End- Current Queue Status:
- rAF Queue:
[rAF callback]
- Task Queue:
[M1]
- Idle Queue:
[Idle callback]
- rAF Queue:
- Current Queue Status:
- Macrotask: The main thread starts executing the global script, which is the first macrotask.
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]
- Task Queue:
- Current Queue Status:
- 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.
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, andMessageChannel 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]
- Task Queue:
- Current Queue Status:
- Macrotask: The event loop takes Macrotask M1 from the standard task queue based on the FIFO principle and executes it.
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]
- Task Queue:
- Current Queue Status:
- Macrotask: The event loop continues to take Macrotask M2 from the standard task queue and executes it.
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 (✅).
- Execute the callback for Microtask C, and
- Tick 5 End
- Current Queue Status:
- Task Queue:
[M3]
- Idle Queue:
[]
- Task Queue:
- Current Queue Status:
- 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.
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.
- Macrotask: The event loop takes Macrotask M3 from the standard task queue and executes it.
Flow Summary
Tick | Macrotask Type | Console Output |
---|---|---|
1 | Initial Script | (None) |
2 | requestAnimationFrame | requestAnimationFrame requestAnimationFrame - Promise |
3 | MessageChannel (First) | MessageChannel Callback MessageChannel - 1 |
4 | MessageChannel (Second) | MessageChannel Callback MessageChannel - 2 |
5 | requestIdleCallback | requestIdleCallback requestIdleCallback - Promise |
6 | MessageChannel (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
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);
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 about16.7ms
per frame). In the logs,requestAnimationFrame
executes within a few milliseconds after thesetInterval
callback, as expected. - Background: When a tab is not active, the browser pauses the execution of
requestAnimationFrame
callbacks. All callbacks scheduled viarequestAnimationFrame
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 newrequestAnimationFrame
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 onrequestAnimationFrame
, then all React updates (including state updates, side effect handling, etc.) will stop when thetab
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'scommit
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 withinrequestAnimationFrame
is not reasonable.
- Background tab pause: When a
- Foreground: The callback function executes before the browser's next repaint. This is typically synchronized with the display's refresh rate (e.g.,
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 fromrequestAnimationFrame
. Even in the background, if the browser determines there are idle resources,requestIdleCallback
callbacks may still be executed. However, the execution frequency and availabletimeRemaining()
of backgroundrequestIdleCallback
might be limited.Refocus: Similar to
requestAnimationFrame
, ifrequestIdleCallback
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 theTab
is reactivated.Feasibility Analysis
Background tab pause:
requestIdleCallback
still has a chance to execute in backgroundtabs
, although its frequency and reliability may decrease. This is better than the complete pause ofrequestAnimationFrame
. The execution timing ofrequestIdleCallback
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 adeadline.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 therequestIdleCallback
feature, andIE
andOpera Mini
do not support it at all. User adoption is only78.95%
.Hack behavior:
requestIdleCallback
can be configured with atimeout
parameter to push low-priority tasks that time out to the host environment's macrotask queue. At the same time, tests show that whentimeout > 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, usingrequestIdleCallback(fn, { timeout: 1 })
can, to some extent, complete scheduling tasks.However, this is a
hack
behavior and goes against the original design intention ofrequestIdleCallback
. The original design intention ofrequestIdleCallback
is to schedule tasks as low-priority tasks, to be scheduled when the host environment is idle or when tasks are starving. The meaning ofrequestIdleCallback(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 oftimeout: 1
pushing macrotasks is not as high assetTimeout(fn, 0)
andnew MessageChannel()
. Meanwhile, thehack
method goes against the original intention ofrequestIdleCallback
, andrequestIdleCallback
also has compatibility and cross-platform issues, so choosingrequestIdleCallback
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
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);
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:
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();
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();
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');
}
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();
};
export const SchedulerType = {
MessageChannel: 'MessageChannel',
SetTimeout: 'SetTimeout',
SetImmediate: 'SetImmediate'
};
Output Logs in a Browser Host Environment
--- 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
--- 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:
setImmediate
The time from scheduling to triggering a task is almost always
0ms
, demonstrating stability. This shows thatsetImmediate
, as its name suggests, executes immediately and is semantic.setTimeout
In both
node.js
and browser environments, the execution time ofsetTimeout
fluctuates. WhensetTimeout
is configured to0ms
and the tab is in focus, it still exhibits a delayed execution characteristic, with the delay stabilizing around4ms
, especially in deeply nested calls. In other words, in client-side scenarios (client-side rendering or hydration), ifsetTimeout(..., 0)
is used forreact
's scheduling, an additional4ms
delay cost is incurred when there are more than5
tasks, which does not align withreact
's efficient scheduling strategy.In unfocused scenarios,
setTimeout
also shows a phenomenon of low-frequency delayed scheduling, affecting scheduling efficiency in background scenarios.MessageChannel
Under the
MessageChannel
scheduler, the execution time is almost always0ms
, indicating stability. However, in thenode.js
environment, the initial execution time ofMessageChannel
is relatively long, and the process does not exit when there are no more tasks, requiring manual closing of theMessageChannel
'sport
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:
// 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.
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.
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?
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
:
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
:
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.
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.
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()
.
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?
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
, andnetwork
resources. - Avoiding Unnecessary
javascript
Execution: The simulated mechanism inreact scheduler
essentially still runs at thejavascript
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 thejavascript
engine. In contrast, the nativeAbortController
can directly notify the browser kernel to stop the relevant activities, reducing unnecessary script execution and potential event loop congestion.
- 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
Reliability in Complex Scenarios
Unified Signal Propagation:
AbortSignal
provides a clear and consistent way to propagate a cancellation signal. A singleAbortSignal
can be passed to multiple different asynchronous operations. Whenabort()
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
Poor Browser Compatibility
- Supported: Mainly in
chromium-based
browsers (chrome
,edge
,opera
) since around version94
. - Not Supported:
firefox
support starts from the latestNightly
version142
.safari
does not support it at all.
- Supported: Mainly in
chromium-based
browsers (chrome
,edge
,opera
) since around version129
. - Not Supported:
firefox
support starts from the latestNightly
version142
.safari
does not support it at all.
- Supported: Mainly in
Insufficient Features
No Starvation Handling Mechanism:
react scheduler
can set anexpirationTime
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 nativescheduler API
currently lacks this mechanism.