How to Schedule Background Tasks in JavaScript
If you remember nothing else about JavaScript, never forget this: it blocks.
Imagine a magical processing pixie makes your browser work. Everything is handled by that single pixie whether it’s rendering HTML, reacting to a menu command, painting on the screen, handling a mouse click or running a JavaScript function. Like most of us, the pixie can only do one thing at a time. If we throw many tasks at the pixie, they get added to a big to-do list and are processed in order.
Everything else stops when the pixie encounters a script
tag or has to run a JavaScript function. The code is downloaded (if required) and run immediately before further events or rendering can be handled. This is necessary because your script could do anything: load further code, remove every DOM element, redirect to another URL etc. Even if there were two or more pixies, the others would need to stop work while the first processed your code. That’s blocking. It’s the reason why long-running scripts cause browsers to become unresponsive.
You often want JavaScript to run as soon as possible because the code initializes widgets and event handlers. However, there are less important background tasks which don’t directly affect the user experience, e.g.
- recording analytics data
- sending data to social networks (or adding 57 ‘share’ buttons)
- pre-fetching content
- pre-processing or pre-rendering HTML
These are not time-critical but, in order for the page to remain responsive, they shouldn’t run while the user is scrolling or interacting with the content.
One option is to use Web Workers which can run code concurrently in a separate thread. That’s a great option for pre-fetching and processing but you’re not permitted to directly access or update the DOM. You can avoid that in your own scripts but you can’t guarantee it’ll never be required in third-party scripts such as Google Analytics.
Another possibility is setTimeout
, e.g. setTimeout(doSomething, 1);
. The browser will execute the doSomething()
function once other immediately-executing tasks have completed. In effect, it’s put on the bottom of the to-do list. Unfortunately, the function will be called regardless of processing demand.
requestIdleCallback
requestIdleCallback is a new API designed to schedule non-essential background tasks during those moments the browser is taking a breather. It’s reminiscent of requestAnimationFrame which calls a function to update an animation before the next repaint. You can read more about requestAnimationFrame
here: Simple Animations Using requestAnimationFrame
We can detect whether requestIdleCallback
is supported like so:
if ('requestIdleCallback' in window) {
// requestIdleCallback supported
requestIdleCallback(backgroundTask);
}
else {
// no support - do something else
setTimeout(backgroundTask1, 1);
setTimeout(backgroundTask2, 1);
setTimeout(backgroundTask3, 1);
}
You can also specify an options object parameter with a timeout (in milliseconds), e.g.
requestIdleCallback(backgroundTask, { timeout: 3000; });
This ensures your function is called within the first three seconds, regardless of whether the browser is idle.
requestIdleCallback
calls your function once only and passes a deadline
object with the following properties:
didTimeout
— set true if the optional timeout firedtimeRemaining()
— a function which returns the number of milliseconds remaining to perform a task
timeRemaining()
will allocate no more than 50ms for your task to run. It won’t stop tasks exceeding this limit but, preferably, you should call requestIdleCallback
again to schedule further processing.
Let’s create a simple example which executes several tasks in order. The tasks are stored in an array as function references:
// array of functions to run
var task = [
background1,
background2,
background3
];
if ('requestIdleCallback' in window) {
// requestIdleCallback supported
requestIdleCallback(backgroundTask);
}
else {
// no support - run all tasks soon
while (task.length) {
setTimeout(task.shift(), 1);
}
}
// requestIdleCallback callback function
function backgroundTask(deadline) {
// run next task if possible
while (deadline.timeRemaining() > 0 && task.length > 0) {
task.shift()();
}
// schedule further tasks if necessary
if (task.length > 0) {
requestIdleCallback(backgroundTask);
}
}
Is There Anything That Shouldn’t Be Done In a requestIdleCallback?
As Paul Lewis notes in his blog post on the subject, the work you do in a requestIdleCallback should be in small chunks. It is not suitable for anything with unpredictable execution times (such as manipulating the DOM, which is better done using a requestAnimationFrame callback). You should also be wary of resolving (or rejecting) Promises, as the callbacks will execute immediately after the idle callback has finished, even if there is no more time remaining.
requestIdleCallback Browser Support
requestIdleCallback
is an experimental feature and the spec is still in flux, so don’t be surprised when you encounter API changes. It’s supported in Chrome 47 … which should be available before the end of 2015. Opera should also gain the feature imminently. Microsoft and Mozilla are both considering the API and it sounds promising. There’s no word from Apple as usual. If you fancy giving it a whirl today, your best bet is to use Chrome Canary (a much newer release of Chrome that’s not as well tested, but has the latest shiny stuff).
Paul Lewis (mentioned above) created a simple requestIdleCallback shim. This implements the API as described but it’s not a polyfill which can emulate the browser’s idle-detection behavior. It resorts to using setTimeout
like the example above but it’s a good option if you want to use the API without object detection and code forking.
While support is limited today, requestIdleCallback
could be an interesting facility to help you maximize web page performance. But what do you think? I’d be glad to hear your thoughts in the comments section below.