Skip to main content

Getting Started With scheduler.yield - A Beginner's Guide

· Updated on · 13 min read
Umar Hansa

This post introduces the scheduler.yield API and explains how you can use it to optimise the performance of your web applications. If you want to follow along with the code snippets or interactive demos, be sure to read the browser support section to ensure that your browser supports this feature.

Introduction

In the context of web performance, scheduling is the browser's way of deciding which tasks to run (JavaScript, page rendering, etc.) and when to run them. Scheduling is a key concept that can impact the user experience of your web applications.

While not every website needs to worry about scheduling, scheduling is an important feature to understand as it offers precise timing and control over how your JavaScript is scheduled.

What is scheduler.yield?

info

The scheduler.yield API is not yet supported in all browsers.

scheduler.yield() is an experimental method available in JavaScript:

scheduler.yield(); // Promise {<pending>}

The scheduler.yield() method is used to yield control back to the browser's scheduler, allowing other important tasks to run. It's the browser that decides what's considered "important".

This can be useful when you want to ensure that your JavaScript code doesn't block the main thread and negatively impact the user experience.

You might be wondering, what other work does the browser need to do? The browser scheduler is responsible for a lot of things, such as:

  • Rendering the page
  • Garbage collection
  • Handling user input

Let's focus on the last one, handling user input. When the main thread is blocked, the browser can't respond to most types of user input. This can lead to a poor user experience and result in poor web performance metric scores, like Interaction to Next Paint as part of Core Web Vitals.

In the following diagram, the "Before scheduler.yield" and "After scheduler.yield" examples both take 3.5 seconds to complete. However, the "After scheduler.yield" version is able to run an important task related to user input after 1 second, while the "Before scheduler.yield" example is only able to run the important task after 3 seconds.

Before and after of using scheduler.yield

If you find terms like "main thread", "browser scheduler" and "blocking" confusing, know that you're not alone! Traditionally, web performance was centered around network performance, but as web applications have become more complex, the focus has shifted to runtime performance - especially for JavaScript-heavy web apps.

  • Main thread: A browser process that handles different tasks like rendering the page.
  • Browser scheduler: The browser scheduler decides which tasks to run and when to run them.
  • Blocking: When a task is running on the main thread, it can stop other tasks from running.

Note on code examples

In the next few sections, you'll see code examples that demonstrate how to use scheduler.yield. Before that however, we first need an example of a slow running function that blocks the main thread. The implementation of the following function isn't important:

// A helper function to block the main thread, for demonstration purposes only
function blockMainThread(duration) {
const startTime = Date.now();
while (Date.now() - startTime < duration) {
// Blocking the main thread
}
}

The main takeaway here is that when blockMainThread is called, it blocks the main thread for (roughly) the specified duration.

A basic example of scheduler.yield

In this section, you'll see a minimal before and after example of using scheduler.yield.

info

A task that blocks even for 500ms is a Long Task and needs fixing.

First, the "before" which blocks the main thread for 1 second, and does not use scheduler.yield:

Before:

// This function blocks for 1 continuous second (500ms + 500ms)
function blocksContinuously() {
blockMainThread(500);
blockMainThread(500);
}

And here's the "after" which does uses scheduler.yield:

After:

async function blocksInChunks() {
// Blocks for 500ms, then yields to the browser scheduler
blockMainThread(500);

await scheduler.yield(); // The browser scheduler can run other tasks at this point

// Blocks for another 500ms and returns
blockMainThread(500);
}

And here's the difference shown in a DevTools Performance profile:

DevTools screenshot of a before and after of using scheduler.yield

Here are some key points to note from the DevTools screenshot:

Without scheduler.yieldWith scheduler.yield
Button responsivenessThe button :active state freezes for 1 secondThe button active state freezes for 500ms
User experienceThe user is unable to interact with the page for 1 secondThe user can interact with the page after 500ms
Long TaskThe Long Task takes 1 second to completeThe Long Task takes 500ms to complete
tip

scheduler.yield returns a promise. After the promise is resolved, the remaining code in blocksInChunks will run.

There's a big difference between the two implementations shown earlier:

  • Without scheduler.yield: The blocksContinuously function blocks the main thread for 1 continuous second.
  • With scheduler.yield: The blocksInChunks function blocks for 500ms, yields to the browser scheduler, and then blocks for another 500ms.

For many website use cases, it's inevitable that some JavaScript will block the main thread. However there's a big difference in blocking the main thread for 1 continuous second, versus blocking for 500ms, yielding to the browser scheduler, and then blocking for another 500ms. In the example case of a 500ms blocking task, you would go a step further and break down the 500ms block into smaller chunks, while yielding to the browser scheduler in between each chunk.

The previous example is kept minimal for educational purposes. You might be wondering, how would DevTools present this information when a user interaction occurs. Continue with this article to learn more.

Demo: using scheduler.yield with a user interaction

Here's a interactive demo of using scheduler.yield that highlights a user interaction scenario.

Screenshot of the interactive demo

Follow these steps to see how using scheduler.yield results in a faster interaction:

  1. Choose either "One Long Task" or "Many Smaller Tasks" button.
  • One Long Task: Does not use scheduler.yield.
  • Many Smaller Tasks: Uses scheduler.yield.
  1. Immediately after clicking one of those buttons, quickly click the "Accept Cookies" button.
  2. Observe the delay in being notified "Here are your cookies"

Bonus: Record a DevTools performance profile of the above steps. Can you observe how JavaScript execution appears differently depending on whether you clicked "One Long Task" or "Many Smaller Tasks"? If not, continue reading to see what it looks like.

In this scenario, there is a long blocking task that does not use scheduler.yield. During the task, the user clicks on an "Accept cookies" button. The browser is unable to respond to this user input until the long task is complete.

Note if you are not sure on what to focus on within the following screenshot, skip it, and look at the next one.

DevTools screenshot of long blocking task

Here's an annotated version of the previous screenshot:

DevTools annotated screenshot of long blocking task

In this next scenario, the long blocking task has been split up into chunks, and scheduler.yield is used throughout. During the task, the user clicks on an "Accept cookies" button. The browser is able to respond to this user input after the first chunk of work is complete.

Note if you are not sure on what to focus on within the following screenshot, skip it, and look at the next one.

DevTools screenshot of task that has been split up into chunks

Here's an annotated version of the previous screenshot:

DevTools screenshot of task that has been split up into chunks

tip

You can install the Site Speed by DebugBear Extension to see an overlay of Interaction to Next Paint (INP) times on your website, amongst other metrics.

Site Speed by DebugBear extension for the basic scheduler.yield demo

Cancelling a scheduled task after yielding with a TaskController

The continuation of work is the code that runs after the scheduler.yield call. You can cancel this continuation by using the following:

The main point to highlight in the following code example is that the user can start a long running task by clicking the startButton, but the user can also cancel that long running task by clicking the cancelButton:

let controller;

async function longTask() {
try {
blockMainThread(500);

// Yield the task using the scheduler, passing the signal from the controller
// The "inherit" option means the task will inherit the signal from its parent
await scheduler.yield({ signal: "inherit" });

// If the user cancels the task, the code below will not run
blockMainThread(500);
} catch (error) {
if (error.name === "AbortError") {
// Handle the abort
}
}
}

startButton.addEventListener("click", () => {
controller = new TaskController();

// Post the longTask to the scheduler, passing the signal from the controller
scheduler.postTask(longTask, {
signal: controller.signal,
});
});

cancelButton.addEventListener("click", () => {
if (controller) {
controller.abort();
}
});

In the previous code, take note of the following:

  1. The scheduler.yield function throws an AbortError if the task is aborted.
  2. A good separation of concerns is achieved by using a TaskController to handle the cancellation - the longTask function does not need to know of the mechanism used to cancel it.
  3. Notice that the longTask() function does not need modification to support cancellation. The TaskController and scheduler.postTask take care of this.

Here's an interactive demo to show the cancellation of the continuation. To try this feature out:

Screenshot of the interactive demo

  1. Click 'Start work'.
  2. Shortly after, click 'Cancel work'.
  3. Observe that the task is cancelled and the output is updated.

Browser support

scheduler.yield is currently an experimental web standard, so you need to opt in first before using it. You can either do that locally in your browser or by signing your website up to try out the new feature.

Use in your local development environment

To test scheduler.yield locally you can set a flag in Chrome. To enable it:

  1. Go to chrome://flags.
  2. Search for "Experimental Web Platform features" and enable it.
  3. Once enabled, you'll need to restart your browser for the changes to take effect.

Within your JavaScript code, you can use this check to see if scheduler.yield is available:

if ("scheduler" in window && "yield" in scheduler) {
console.log("scheduler.yield is supported");
} else {
console.log("scheduler.yield is not supported");
}

Use in production

If you want to use this feature in production, you'll need to sign up for the Origin Trial.

Origin trials give you access to a new or experimental feature, to build functionality your users can try out for a limited time before the feature is made available to everyone.

Fallback for unsupported browsers

A polyfill is also available for browsers that don't support scheduler.yield. You can find it on GitHub or on npm.

If you don't need all the features of scheduler.yield, you can consider this alternative fallback technique that involves using setTimeout to yield control back to the browser scheduler:

async function blocksInChunks() {
// Blocks for 500ms
await blockMainThread(500);

// Yields to the browser scheduler
await new Promise((resolve) => setTimeout(resolve, 0));

// Blocks for another 500ms
await blockMainThread(500);
}

While the behaviour of setTimeout is not exactly the same as scheduler.yield, you should still see an improvement in your web performance metrics when you use either technique.

The exact differences between scheduler.yield and setTimeout are out of scope for this article, but one important takeaway is:

  • With scheduler.yield, the continuation of work is placed at the front of the task queue.
  • With setTimeout, the continuation of work is placed at the end of the task queue.

Generally speaking, scheduler.yield offers more predictable scheduling behaviour than setTimeout.

Consider the following code examples, and take note of their respective outputs:

With scheduler.yield (preferred):

// Adds to the task queue
setTimeout(() => console.log("Timeout callback"), 0);

console.log("Yielding with scheduler.yield");
await scheduler.yield();
console.log("Continuation after scheduler.yield");
Output of scheduler.yield

Yielding with scheduler.yield

Continuation after scheduler.yield

Timeout callback

With setTimeout (fallback):

// Adds to the task queue
setTimeout(() => console.log("Timeout callback"), 0);

console.log("Yielding with setTimeout");
await new Promise((resolve) => setTimeout(resolve, 0));
console.log("Continuation after setTimeout");
Output of setTimeout

Yielding with setTimeout

Timeout callback

Continuation after setTimeout

In both examples, take note of when the continuation of work occurs.

How DebugBear helps monitor the impact of scheduler.yield

DebugBear is a web performance monitoring tool that helps you track the performance of your web applications. DebugBear shows you:

  • Interaction to Next Paint scores from CrUX.
  • Specific Interaction to Next Paint data per-page view.
  • A visualisation of your Total Blocking Time metric.
  • A visualisation of your CPU activity on the main thread.

When you use scheduler.yield, you can monitor the impact on your web performance metrics:

  • Your overall Interaction to Next Paint (INP) score should improve, as the browser can respond to user input more quickly as you chunk your work and frequently yield to the browser scheduler.

    Where available, we display INP data from CrUX in your DebugBear report.

  • When you use DebugBear's Real User Monitoring feature, we display INP data per-page view. You also get an component level breakdown of INP, which can help you identify which parts of your page are causing the most delay.

    INP data in DebugBear

    In the previous example, a high input delay is observed. This is because the main thread is blocked and the browser is unable to respond to user input. If you regularly yield back to the browser scheduler, you can reduce this input delay.

  • Your Total Blocking Time score may improve, as you work to better orchestrate your JavaScript work, chunk tasks into smaller pieces, and minimise the tasks themselves.

    Total blocking time in DebugBear

  • A DebugBear report shows you activity that was blocking the CPU. You can view this on your timeline.

    CPU activity in DebugBear

Conclusion

The scheduler.yield API helps you provide a better user experience for your users by yielding control back to the browser scheduler. This can help prevent your JavaScript code from blocking the main thread.

You should aim to to split your work into smaller chunks and yield to the browser scheduler frequently, but you should also consider where it makes sense to do so. For example, you might not want to yield control back to the browser scheduler in the middle of a critical operation.

You should consider for other scenarios. What happens when:

  • The scheduler.yield rejects the promise and your work is not completed.
  • The page is closed before the scheduler.yield promise is resolved.
  • The delay between the scheduler.yield promise being resolved and the continuation of work is unexpectedly long.

Take note that scheduler.yield is not a magic bullet. For example, when you yield to the browser scheduler, the browser might decide to run another task that blocks the main thread. You should also consider that interspersing your existing code with scheduler.yield calls is not guaranteed to improve performance.

Further reading

Get a monthly email with page speed tips