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.

Paul LewisGoogle

Glossary

1.0 Why?
2.0 How?
   2.1 The requestIdleCallback Function
   2.2 Deadline Object and the Idle timeRemaining Property
   2.3 Wrapping rIC with Promises
4.0 Availability
5.0 References

1. Why?

As JavaScript is executed on a single thread any computation causes the application to wait until it has completed before moving on and performing another. There is no standard for delaying execution of a procedure until the environment is idle… until now! When an environment is idle can be the best time to execute tasks that don’t make an immediate and obvious contribution to the user’s experience. With requestIdleCallback [1] we can supply a callback to be invoked when the environment isn’t occupied with anything else and therefore avoid interupting user interactions.

2. How?

2.1 The requestIdleCallback Function

We simply pass an anonymous function or function reference to requestIdleCallback. We also have the option of a second argument that specifies the maximum amount of time to wait before executing the callback outright, regardless of whether the browser is idle. This is really useful for tasks that are necessary without requiring immediate attention.

Using the requestIdleCallback function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Passing an anonymous function
// -----------------------------
requestIdleCallback(function someHeavyComputation(deadline) {
// do some heavy work...
});

// Passing reference to a method
// -----------------------------
let someObject = {};
someObject.someHeavyComputation = function someHeavyComputation(deadline) {
// do some heavy work...
};
requestIdleCallback(someObject.someHeavyComputation);

// Passing anonymous function with a timeout
// -----------------------------------------
requestIdleCallback(function someHeavyComputation(deadline) {
// do some heavy work...
}, 5000);

2.2 Deadline Object

You may have noticed in each of the examples above that the callback is invoked with a single argument deadline. This is an object with two properties: timeRemaining and didTimeout.

timeRemaining

timeRemaining contains the amount of time that the function has left before the environment is no longer idle (in milliseconds). It is updated dynamically and so can be inspected within the procedure via a while loop or similar such that it will only perform its operations during the available idle window. This is necessary because as with any function the browser cannot prematurely destroy your callback. You are therefore responsible for ending execution of the callback (via a return) at an appropriate time (timeRemaining >= 0) so that it does not occupy more than is reported by the browser as being idle! If you don’t do this you will start muscling in on time that would have been used for other operations.

Ensuring continued execution of idle work callback
1
2
3
4
5
6
7
8
9
requestIdleCallback(function someHeavyComputation(deadline) {
while(deadline.timeRemaining > 0) {
// do some heavy work...
}

if(thereIsMoreWorkToDo) {
requestIdleCallback(someHeavyComputation);
}
});

didTimeout

didTimeout is a boolean that if true means that the callback has been invoked because an idle window was not available within the deadline provided via requestIdleCallback‘s second argument. If you have provided this argument and the amount of time declared by it has passed you will want to perform the callback’s work by checking the didTimeout property and doing the work regardless.

Checking if request timed out
1
2
3
4
5
6
7
// Declare callback to be invoked after two seconds if
// no idle window is available within that time
requestIdleCallback(function someHeavyWork(deadline) {
if(deadline.timeRemaining > 0 || deadline.didTimeout) {
// do some heavy work...
}
}, 2000);

2.3. Wrapping rIC with Promises

You can of course execute any code within a requestIdleCallback (rIC), but if the operation you are performing is computationally expensive and will therefore take a long time to complete, consider wrapping it in a Promise [2]. Doing this will abstract a linear operation that would otherwise be blocking and will make it obviously asynchronous.

Wrapping requestIdleCallback with a Promise
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
function factorial(input) {
if(!('requestIdleCallback' in window)) {
console.log('requestIdleCallback is not available in this environment');
return;
}

// Wrap the call to requestIdleCallback in a Promise
return new Promise(function(resolve, reject) {
let start = Date.now();
let n = input - 1;
let result = input;

// First call to start the work in the next idle window
requestIdleCallback(next);

function next(deadline) {
if(deadline.timeRemaining <= 0) {
requestIdleCallback(next);
return;
}

result *= n--;

if(n <= 1) {
// Calculation has completed, `resolve` the Promise
return resolve({
num: result,
time: (Date.now() - start) / 1000
});
}

// Perform the next calculation, passing in the `deadline`
// again so that the function knows not to continue
// if the `timeRemaining` is zero
next(deadline);
}
});
}

// Calculate the factorial of 20 asynchronously
factorial(20)
.then(function(result) {
console.log(
'Result: ' + result.num, // 2432902008176640000
'Execution time: ' + result.time + 's'
);
// heavy work done...
})
.catch(function(err) {
// handle error...
console.log(err);
});

4. Availability

Currently this feature is only available in Chrome Canary (with chrome://flags/#enable-experimental-web-platform-features enabled). But there have been talks with the other browser vendors Microsoft and Apple about implementing this in their own browsers. [3]

Chrome (Canary) Firefox Safari Opera
Yes No No No


While availablity is poor you will need to always check that the requestIdleCallback API is available with a simple if block.

Checking for requestIdleCallback on window
1
2
3
4
5
if ('requestIdleCallback' in window) {
window.requestIdleCallback(someFunctionToCall);
} else {
console.log('requestIdleCallback not available in this environment');
}

5. References

[1] “Using requestIdleCallback” on the Google Developers Blog
[2] Promise Ponderings, (Anti-)Patterns, and Apologies
[3] Paul Lewis on Twitter