How to yield to the main thread

Yield to the main thread to improve page responsiveness

Arjen Karel Core Web Vitals Consultant
Arjen Karel - linkedin
Last update: 2024-10-03

Yield to the main thread

Imagine a romantic movie. The setting is a small French marketplace in the center of a small village. The streets are filled with couples drinking coffee, eating croissants and buying flowers. Now image what happens when only 1 seller can buy something from a seller simultaneously while all others have to wait their turn. The baker gets swamped with requests, arguments break out at the florist and the romantic stroll turns into a frustrating wait.

Well ...  that's kind of what happens on a website when things get too busy.

The Importance of Yielding

The main thread of a browser handles all the important processes and schedules all important tasks (parsing HTML and CSS, executing JavaScript code, handling input events like clicks and scrolls, and visual rendering ). 

The main thread of a browser runs on a single-threaded model This means that it can only perform one task at a time. When a task (usually JavaScript execution or rendering) start to run the browser will run this taks 'to completion' and will not stop until it is done. This means it will not schedule or perform any other task until the previous task is ready. This as called 'blocking the main thread'. Blocking the main thread is a problem when visitors interact with a page because the page will be unresponsive during blocking time . 

One way to fix the blocking of the main thread is by 'Yielding tot he main thread'. Yielding is a technique where long tasks are broken down into multiple smaller tasks  to allow the main thread to handle more important tasks (like user input). 

Long tasks and blocking period: When a task takes longer than 50 milliseconds, it's classified as a long task, and anything beyond that 50-millisecond threshold is known as the task's 'blocking period'. Breaking up these long tasks into smaller chunks allows the browser to remain responsive, even when handling computationally intensive operations.

Old yielding strategies

Before the new Prioritized Task Scheduling API there were 4 ways to yield to the main thread. All have their limitations and concerns!

  • setTimeout(): The most common strategy, setTimeout() works by executing code in a callback function after a specified delay. By setting a delay (or timeout) of 0. setTimeout() adds the the task to the end of the queue, allowing other tasks to run first. One major problem is that, setTimeout() isn't designed for precise scheduling because tasks can only be pushed to the end of the queue.
  • requestAnimationFrame(): requestAnimationFrame() works by queueing a function to be executed before the browser's next repaint. requestAnimationFrame() is often combined with setTimeout() to ensure callback functions get scheduled after the next layout update.
  • requestIdleCallback(): This method is best suited for non-critical, low-priority tasks that can be executed during a browser's downtime. While it helps to keep the main thread free for more crucial tasks, requestIdleCallback() suffers from a significant limitation: there's no guarantee that the scheduled tasks will run promptly (or ever!!!), especially on a busy main thread.
  • isInputPending(): isInputPending() works by checking for pending user inputs and yielding only if an input is detected. isInputPending() can be used to work together with other yielding functions to prevent unnecessary yielding. Unfortunately, it can return false negatives and doesn't address the need to yield for other performance-critical tasks like animations.

Introducing: scheduler.yield()

The limitations of these 4 methods have been a concern for the chrome team, especially with many sites failing the Interaction to Next Paint metric. To fix this and help, the Chrome team has built a  new APIs that give granular control over  javascript scheduling.

Meet scheduler.yield()! With window.scheduler.yield() developers can effectively yield to the main thread immediately without rearranging the order of tasks or creating extra complexity.

Use Scheduler.yield() effectively

Let's dive into the code and explore how to leverage scheduler.yield() the way it is meant to be! Before we start using scheduler.yield(), it's important to check for browser compatibility because it's an experimental feature and not all browsers support it yet. 

Example code

async function yieldToMain() {
  if ('scheduler' in window && 'yield' in window.scheduler) {
    return await window.scheduler.yield();
  }
  return new Promise((resolve) => {
    setTimeout(resolve, 0);
  });
}

The yieldToMain() function is the core of yielding control to the main thread. It first checks that both window.scheduler and window.scheduler.yield exist. If it does we can safely use window.scheduler.yield. If  window.scheduler.yield is not supported a the code falls back to setTimout).

Shorter and production ready version

Or use this shorter, production ready version that uses the ternary operator:

/* or shorter and production ready: */
async function yieldToMain(){
  return"scheduler"in window&&"yield"in window.scheduler?
  await window.scheduler.yield():new Promise(e=>{setTimeout(e,0)})
}

Real life example: Enhancing Search with yieldToMain():

Let's see how we can use yieldToMain() to improve the search experience for your users:

The handleSearch() function demonstrates how yieldToMain() can be used effectively. It first updates the button content to provide immediate feedback that a search is in progress. You can use yieldToMain() here to allow the browser to update the layout.

Next, fetchData() retrieves search data, and updateHTML(data) displays the results. Another  yieldToMain() ensures a quick layout update (since we can never be sure there aren't other event listeners waiting after this task).  Finally other, less important tasks are scheduled during browser idle time. Not that I did not yield to the main thread here since requestIdleCallback() will only run when the main thread is idle.

async function handleSeach(){
 /* quickly update the button content after submitting*/
 await updateButtontoPending();

 /* Yield to Main */
 await yieldToMain();

 /* fetch data and update html*/
 const data = await fetchData();
 await updateHTML(data);

 /* Yield to Main again */
 await yieldToMain();

 /* some function should only run during browser idle time*/
 requestIdleCallback(sendDataToAnalytics);
}

Why schedulier.yield() is just better

Unlike setTimeout(), which adds deferred tasks to the end of the task queue, scheduler.yield() only pauses the javascript queue and paused tasks remain at the front of the queue, ensuring they are executed as soon as possible after higher-priority tasks (like handling input callbacks) have been completed. This "front-of-queue" behavior addresses the biggest problem of the old yielding methods. And developers can now 'yield to the main thread' without the risk of their important tasks being delayed by other less-important tasks.

yielding timeline

Another advantage of scheduler.yield() is that it is designed to work with the Prioritized Task Scheduling API like scheduler.postTask(), which allows for prioritizing tasks based on their importance. This combination of features provides developers with a powerful toolkit for optimizing their web applications for responsiveness and performance.


How to yield to the main threadCore Web Vitals How to yield to the main thread