JavaScript - requestIdleCallback

The Cooperative Scheduling of Background Tasks API (also referred to as the Background Tasks API or simply the requestIdleCallback() API) provides the ability to queue tasks to be executed automatically by the user agent when it determines that there is free time to do so.

Background Tasks API - Web APIs | MDN

Idle Until Urgent

A few weeks ago I was looking at some of the performance metrics for my site. Specifically, I wanted to see how I was doing on our newest metric, first input delay (FID). My site is just a blog (and doesn’t run much JavaScript), so I expected to see pretty good results.

Input delay that’s less than 100 milliseconds is typically perceived as instant by users, so the performance goal we recommend (and the numbers I was hoping to see in my analytics) is FID < 100ms for 99% of page loads.

To my surprise, my site’s FID was 254ms at the 99th percentile. And while that’s not terrible, the perfectionist in me just couldn’t let that slide. I had to fix it!

[…]

[W]hile I was trying to solve my issue I stumbled upon a pretty interesting performance strategy that I want to share (it’s the primary reason I’m writing this article).

I’m calling the strategy: idle until urgent.

My performance problem

First input delay (FID) is a metric that measures the time between when a user first interacts with your site (for a blog like mine, that’s most likely them clicking a link) and the time when the browser is able to respond to that interaction (make a request to load the next page).

The reason there might be a delay is if the browser’s main thread is busy doing something else (usually executing JavaScript code). So to diagnose a higher-than-expected FID, you should start by creating a performance trace of your site as it’s loading (with CPU and network throttling enabled) and look for individual tasks on the main thread that take a long time to execute. Then once you’ve identified those long tasks, you can try to break them up into smaller tasks.

Here’s what I found when doing a performance trace of my site:

So what’s taking so long to run?

Well, if you look at the tails of this flame chart, you won’t see any single functions that are clearly taking up the bulk of the time. Most individual functions are run in less than 1ms, but when you add them all up, it’s taking more than 100ms to run them in a single, synchronous call stack.

This is the JavaScript equivalent of death by a thousand cuts.

Since the problem is all these functions are being run as part of a single task, the browser has to wait until this task finishes to respond to user interaction. So clearly the solution is to break up this code into multiple tasks[.]

[…]

A perfect example of a component that really needs to have its initialization code broken up can be illustrated by zooming closer down into this performance trace. Mid-way through the main() function, you’ll see one of my components uses the Intl.DateTimeFormat API[.]

[…]

Creating this object took 13.47 milliseconds!

The thing is, the Intl.DateTimeFormat instance is created in the component’s constructor, but it’s not actually used until it’s needed by other components that reference it to format dates. However, this component doesn’t know when it’s going to be referenced, so it’s playing it safe and instantiating the Int.DateTimeFormat object right away.

[…]

Idle Until Urgent

After spending a lot of time thinking about this problem, I realized that the evaluation strategy I really wanted was one where my code would initially be deferred to idle periods but then run immediately as soon as it’s needed. In other words: idle-until-urgent.

Idle-until-urgent sidesteps most of the downsides I described in the previous section. In the worst case, it has the exact same performance characteristics as lazy evaluation, and in the best case it doesn’t block interactivity at all because execution happens during idle periods.

Using requestIdleCallback

Many sites and apps have a lot of scripts to execute. Your JavaScript often needs to be run as soon as possible, but at the same time you don’t want it to get in the user’s way. If you send analytics data when the user is scrolling the page, or you append elements to the DOM while they happen to be tapping on the button, your web app can become unresponsive, resulting in a poor user experience.

[…]

The good news is that there’s now an API that can help: requestIdleCallback. In the same way that adopting requestAnimationFrame allowed us to schedule animations properly and maximize our chances of hitting 60fps, requestIdleCallback will schedule work when there is free time at the end of a frame, or when the user is inactive. This means that there’s an opportunity to do your work without getting in the user’s way.

[…]

Why should I use requestIdleCallback?

Scheduling non-essential work yourself is very difficult to do. It’s impossible to figure out exactly how much frame time remains because after requestAnimationFrame callbacks execute there are style calculations, layout, paint, and other browser internals that need to run. A home-rolled solution can’t account for any of those. In order to be sure that a user isn’t interacting in some way you would also need to attach listeners to every kind of interaction event (scroll, touch, click), even if you don’t need them for functionality, just so that you can be absolutely sure that the user isn’t interacting. The browser, on the other hand, knows exactly how much time is available at the end of the frame, and if the user is interacting, and so through requestIdleCallback we gain an API that allows us to make use of any spare time in the most efficient way possible.