Published: March 6, 2025
A page feels sluggish and unresponsive when long tasks keep the main thread busy, preventing it from doing other important work, like responding to user input. As a result, even built-in form controls can appear broken to users—as if the page were frozen—never mind more complex custom components.
scheduler.yield()
is a way of yielding to the main thread—allowing the browser to run any pending high-priority work—then continuing execution where it left off. This keeps a page more responsive and, in turn, helps improve Interaction to Next Paint (INP).
scheduler.yield
offers an ergonomic API that does exactly what it says: execution of the function it's called in pauses at the await scheduler.yield()
expression and yields to the main thread, breaking up the task. The execution of the rest of the function—called the continuation of the function—will be scheduled to run in a new event-loop task.
async function respondToUserClick() {
giveImmediateFeedback();
await scheduler.yield(); // Yield to the main thread.
slowerComputation();
}
The specific benefit of scheduler.yield
is that the continuation after the yield is scheduled to run before running any other similar tasks that have been queued up by the page. It prioritizes the continuation of a task over starting new tasks.
Functions like setTimeout
or scheduler.postTask
can also be used to break up tasks, but those continuations typically run after any already-queued new tasks, potentially risking long delays between yielding to the main thread and completing their work.
Prioritized continuations after yielding
scheduler.yield
is part of the Prioritized Task Scheduling API. As web developers, we don't typically talk about the order in which the event loop runs tasks in terms of explicit priorities, but the relative priorities are always there, such as a requestIdleCallback
callback running after any queued setTimeout
callbacks, or a triggered input event listener usually running before a task queued up with setTimeout(callback, 0)
.
Prioritized Task Scheduling just makes this more explicit, making it easier to figure out what task will run before another, and enables adjusting priorities to change that order of execution, if needed.
As mentioned, the continued execution of a function after yielding with scheduler.yield()
gets a priority higher than starting other tasks. The guiding concept is that the continuation of a task should run first, before moving on to other tasks. If the task is well-behaved code that periodically yields so that the browser can do other important things (like respond to user input), it shouldn't be punished for yielding by getting prioritized after other similar tasks.
Here's an example: two functions, queued up to run in different tasks using setTimeout
.
setTimeout(myJob);
setTimeout(someoneElsesJob);
In this case, the two setTimeout
calls are right next to each other, but in a real page, they could be called in completely different places, like a first-party script and a third-party script independently setting up work to run, or it could be two tasks from separate components being triggered deep in your framework's scheduler.
Here's what that work could look like in DevTools:
myJob
is flagged as a long task, blocking the browser from doing anything else while it's running. Assuming it's from a first-party script, we can break it up:
function myJob() {
// Run part 1.
myJobPart1();
// Yield with setTimeout() to break up long task, then run part2.
setTimeout(myJobPart2, 0);
}
Because myJobPart2
was scheduled to run with setTimeout
within myJob
, but that scheduling runs after someoneElsesJob
has already been scheduled, here's how execution will look:
We've broken up the task with setTimeout
so the browser can be responsive during the middle of myJob
, but now the second part of myJob
only runs after someoneElsesJob
has finished.
In some cases, that might be fine, but usually this isn't optimal. myJob
was yielding to the main thread to make sure the page could stay responsive to user input, not to give up the main thread entirely. In cases where someoneElsesJob
is especially slow, or many other jobs besides someoneElsesJob
have also been scheduled, it could be a long time before the second half of myJob
is run. That was probably not the intention of the developer when they added that setTimeout
to myJob
.
Enter scheduler.yield()
, which puts the continuation of any function invoking it in a slightly higher priority queue than starting any other similar tasks. If myJob
is changed to use it:
async function myJob() {
// Run part 1.
myJobPart1();
// Yield with scheduler.yield() to break up long task, then run part2.
await scheduler.yield();
myJobPart2();
}
Now the execution looks like:
The browser still has the opportunity to be responsive, but now the continuation of the myJob
task is prioritized over starting the new task someoneElsesJob
, so myJob
is complete before someoneElsesJob
begins. This is much closer to the expectation of yielding to the main thread to maintain responsiveness, not giving up the main thread entirely.
Priority inheritance
As part of the larger Prioritized Task Scheduling API, scheduler.yield()
composes well with the explicit priorities available in scheduler.postTask()
. Without a priority explicitly set, a scheduler.yield()
within a scheduler.postTask()
callback will act basically the same as the previous example.
However, if a priority is set, such as using a low 'background'
priority:
async function lowPriorityJob() {
part1();
await scheduler.yield();
part2();
}
scheduler.postTask(lowPriorityJob, {priority: 'background'});
The continuation will be scheduled with a priority that's higher than other 'background'
tasks—getting the expected prioritized continuation before any pending 'background'
work—but still a lower priority than other default or high-priority tasks; it remains 'background'
work.
This means that if you schedule low priority work with a 'background'
scheduler.postTask()
(or with requestIdleCallback
), the continuation after a scheduler.yield()
within will also wait until most other tasks are complete and the main thread is idle to run, which is exactly what you want from yielding in a low-priority job.
How to use the API
For now, scheduler.yield()
is only available in Chromium-based browsers, so to use it you'll need to feature detect and fall back to a secondary way of yielding for other browsers.
scheduler-polyfill
is a small polyfill for scheduler.postTask
and scheduler.yield
that internally uses a combination of methods to emulate a lot of the power of the scheduling APIs in other browsers (though scheduler.yield()
priority inheritance is not supported).
For those looking to avoid a polyfill, one method is to yield using setTimeout()
and accept the loss of a prioritized continuation, or even not yielding in unsupported browsers if that's not acceptable. See the scheduler.yield()
documentation in Optimize long tasks for more.
The wicg-task-scheduling
types can also be used to get type checking and IDE support if you're feature detecting scheduler.yield()
and adding a fallback yourself.
Learn more
For more information on the API and how it interacts with task priorities and scheduler.postTask()
, check out the scheduler.yield()
and Prioritized Task Scheduling docs on MDN.
To learn more about long tasks, how they affect the user experience, and what to do about them, read about optimizing long tasks.