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.
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).
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.
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.DateTimeFormatinstance 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.DateTimeFormatobject 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.
The good news is that there’s now an API that can help:
requestIdleCallback. In the same way that adopting
requestAnimationFrameallowed us to schedule animations properly and maximize our chances of hitting 60fps,
requestIdleCallbackwill 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
requestAnimationFramecallbacks 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 (
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
requestIdleCallbackwe gain an API that allows us to make use of any spare time in the most efficient way possible.