Web Performance Calendar

The speed geek's favorite time of year
2023 Edition
ABOUT THE AUTHOR
Photo of Ivailo Hristov

Ivailo Hristov, CTO and Co-Founder at NitroPack, is a seasoned engineer with over 10 years of experience in web performance. His user-centric approach to web performance has led to category innovations such as automated font subsetting and versatile resource lazy loading.

99.9.

That’s the percentage of origins with a good First Input Delay score on desktop.

Source: HTTP Archive

Seeing these statistics, it’s easy to conclude that the web peaked in responsiveness.

However, we all know that these numbers do not illustrate the real world. You’ve probably experienced a laggy or glitchy website at least once in the past week, right?

The truth is that when everyone passes a metric, it’s one of two things:

  • Everyone truly excels in what the metric measures
  • The metric doesn’t do its job anymore

Unfortunately, with FID, it’s the second one.

Enter Interaction to Next Paint (INP).

INP is Google’s newest responsiveness Core Web Vitals metric, set to replace FID in March 2024.

And while nearly all websites pass FID, the latest data shows that only 78.7% have good INP scores.

In the following lines, we will look at a few techniques that will allow you to be part of this group of sites that truly provide users with a responsive experience.

Read on.

Understanding Interaction to Next Paint

How is INP superior to FID?

The answer is – comprehensiveness.

Unlike FID, which only measures the first interaction, INP assesses a page’s overall responsiveness to user interactions by observing the latency of all qualifying interactions during a user’s visit to a page.

Also, FID only measures the first interaction’s input delay, whereas INP takes into account the input delay, processing time, and presentation delay.

Source: NitroPack

As far as the interaction types that play a role in the final INP score, only the following are observed:

  • Clicking with a mouse.
  • Tapping on a device with a touchscreen.
  • Pressing a key on either a physical or onscreen keyboard.

But beyond all the technicalities that come with INP, it’s essential to truly understand what the metric measures, and Barry Pollard from Google explained it the best:

“INP does not necessarily measure the full cost of an interaction. Some interactions can take a lot of time, and that’s just the nature of things. And that’s absolutely fine as long as we don’t block the website during that time and as long as we provide [visual] feedback to the user that their interactions are being processed.”

Simply put, INP measures the time from the interaction (for example, a mouse click) until the browser is able to update (or paint) the screen.

This immediate visual feedback is truly what users want when interacting with your website.

As Barry mentioned, blocking the entire website when a visitor interacts with it causes rage clicks and bounces. And this blocking is directly related to the main thread.

The Role of The Main Thread in Web Performance and INP

The main thread is the browser’s hardest worker. It is responsible for:

  • executing JavaScript code
  • parsing HTML
  • building the DOM (Document Object Model)
  • rendering the web page (layout calculations and painting)
  • and handling user interactions like clicks, scrolls, and keyboard inputs.

Unfortunately, web browsers typically use a single-threaded model for these operations, meaning the main thread handles almost all tasks required to display and interact with a webpage.

But wait, there’s more:

The main thread can execute only one task at a time.

Long-running or intensive JavaScript tasks, large CSS layouts, and complex rendering can block the main thread. This blocking can lead to a sluggish interface, unresponsive buttons or scrolls, and generally poor user experience.

So, which tasks are considered long?

If a task takes more than 50 milliseconds to be executed, it’s classified as a long task.

If a user interacts with the page while a long task runs, the browser will be delayed in fulfilling the request, leading to the following user experience:

Source: web.dev

So, how can you optimize the main thread so it offers smooth interactions?

One way is to apply yielding.

Strategies for Yielding to the Main Thread

Yielding refers to the practice of periodically pausing long-running tasks or scripts to allow the browser’s main thread to process other critical operations, such as user inputs, animations, and rendering updates.

Before yielding to the main thread


Put another way, by yielding to the main thread, you’re basically breaking down these long tasks into smaller chunks. After executing each chunk, the script yields control back to the browser, allowing it to handle other pending tasks.

After yielding to the main thread

This can be achieved using yielding functions like:

  • scheduler.yield()
  • setTimeout
  • requestAnimationFrame
  • requestIdleCallback

scheduler.yield()

Chrome is currently running an origin trial for scheduler.yield(). It’s a new scheduler API that is expected to give developers and site owners better control over task scheduling.

How so?

Well, scheduler.yield() is a dedicated yield function that sends the remaining work to the front of the queue. This means that work you want to resume immediately after yielding won’t take a back seat to tasks from other sources.

Simply put, you can yield to enhance your site’s responsiveness and INP score and ensure that the work you wanted to finish after yielding isn’t delayed.

How to apply it:

async function processLargeData(data) {
    for (let item of data) {
        // Process each item of data
        processItem(item);

        // Yield control back to the browser's scheduler
        await scheduler.yield();
    }
}

async function processItem(item) {
    // Processing logic here
    console.log(item);
}

// Assuming 'largeDataSet' is an array of data to be processed
processLargeData(largeDataSet);

setTimeout

setTimeout is another JavaScript function used to execute a piece of code after a specified delay. It’s part of the Window interface in the Web API, making it widely available in web browsers.

It’s a strategy commonly used for delaying execution of code to wait for some other task to complete, creating time-based animations or transitions, or debouncing in event handling.

How to apply it:

async function processLargeData(data) {
    for (let item of data) {
        // Process each item of data
        processItem(item);

        // Yield control back to the browser's scheduler using setTimeout
        await new Promise(resolve => setTimeout(resolve, 0));
    }
}

async function processItem(item) {
    // Processing logic here
    console.log(item);
}

// Assuming 'largeDataSet' is an array of data to be processed
processLargeData(largeDataSet);

Just keep in mind that precision isn’t 100% guaranteed. The callback might not run exactly after the specified delay due to other tasks in the queue.

requestAnimationFrame

requestAnimationFrame is a JavaScript method used for creating smooth, efficient animations in web applications.

It tells the browser that you wish to perform an animation and requests that the browser calls a specified function to update an animation before the next repaint. A perfect option for improving your INP score.

The method takes a callback as an argument, executed before the browser’s next repaint, thus allowing animations to run at an optimal frame rate. Also, unlike setTimeout, it’s far more efficient for animations as it synchronizes with the browser’s refresh rate.

How to apply it:

async function processLargeData(data) {
    for (let item of data) {
        // Process each item of data
        await processItem(item);

        // Yield control back to the browser's rendering engine
        await new Promise(resolve => requestAnimationFrame(resolve));
    }
}

async function processItem(item) {
    // Processing logic here
    console.log(item);
}

// Assuming 'largeDataSet' is an array of data to be processed
processLargeData(largeDataSet);

requestIdleCallback

requestIdleCallback is used to schedule tasks during the browser’s idle periods, ensuring that these tasks do not interfere with more critical, user-impacting work like rendering, layout, and user interactions.

This method is particularly useful for running non-urgent background tasks without compromising the performance and responsiveness of the page.

It queues a function to be executed during the browser’s idle periods. The browser provides a deadline object to the callback function, which indicates how much time is available to perform the task. If the task cannot be completed within the available time, it can yield back control and be rescheduled for the next idle period.

It helps keep the main thread free for important tasks, thus improving the web page’s overall performance.

How to apply it:

async function processLargeData(data) {
    for (let item of data) {
        // Process each item of data
        processItem(item);

        // Yield control back to the browser during idle time
        await new Promise(resolve => requestIdleCallback(resolve));
    }
}

function processItem(item) {
    // Processing logic here
    console.log(item);
}

// Assuming 'largeDataSet' is an array of data to be processed
processLargeData(largeDataSet);

Tests and Results

For the purpose of showing the impact of yielding to the main thread, I’ve built a simple page with a single button using CodePen.

In this example, no yielding techniques were applied.

Here are the results:

As you can see from the test, a long task blocked the main thread upon clicking on the button, taking 4202ms for the text to visualize.

In the context of INP, this will result in poor scores and, from March 2024 in failed Core Web Vitals.

Now, let’s see how impactful yielding is when it comes to unblocking the main thread and improving your INP score.

In the second example, we have the exact same page with the only difference that I’ve implemented setTimeout to help the main thread execute the user input:

This is how fast the interaction has been executed:

52.0ms. An improvement of 4150ms. And that’s by using setTimeout, which isn’t the most precise yielding technique.

Nevertheless, it’s the perfect example of how powerful yielding to the main thread is. All you have to do before implementing it is be aware of the drawbacks some of the strategies have.

Conclusion

As we prepare to embrace Interaction to Next Paint as a pivotal metric in assessing web performance, it’s clear that our approach to building and optimizing websites needs to evolve.

This journey isn’t just about adhering to new standards; it’s about redefining the user experience in a digital landscape that is increasingly demanding and dynamic. By implementing strategic yielding techniques, we, as developers, have the opportunity to significantly enhance site responsiveness, creating a seamless interface that resonates with the needs and expectations of modern users.

As we move forward, the focus should remain steadfast on innovation and continuous improvement, ensuring that our digital spaces are not only technically sound but also intuitively aligned with the evolving rhythm of user interactions.

One Response to “Yielding to the Main Thread: How Breaking Up Tasks Can Fix INP”

  1. Web Performance Calendar » Analysis of INP performance using real-world RUMvision data

    […] 2024. INP has already been talked about a few times in this year’s Perf Calendar by Brian, Ivailo, and Sander, so if you need a refresher on what it is and how to optimize it, I’d definitely […]

Leave a Reply

You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>
And here's a tool to convert HTML entities