requestIdleCallback
is a browser API that allows you to schedule tasks to run during browser idle times. This is particularly useful for optimizing performance and allowing non-critical work to be performed without impacting user experience or main thread responsiveness.
Idle times are periods when the browser is not busy processing user input, rendering, or performing other tasks.
Browsers render content in frames, typically at 60 FPS
(60 frames per second
), which means each frame has a budget of approximately 16.67ms
(1000ms / 60 = 16.67ms
). If the browser completes all critical work for a frame in less than 16.67ms
, any remaining time before the next frame is considered an idle period. These idle periods are usually very short, often just a few milliseconds ( 1 ~ 2ms
), and can be used by APIs like requestIdleCallback
to perform background tasks without affecting smooth rendering.
Diagram Loading...
Compared to short idle times, long idle times are more predictable and consistent. They occur when the browser is not busy with any critical work, such as when the user is not interacting with the page or when the page is not rendering any new content. These idle times can last for hundreds of milliseconds or even seconds, depending on user activity and the complexity of the page.
Diagram Loading...
1// function that needs to be executed when the browser is idle
2const doTask = () => {
3 console.log('Doing some work');
4};
5
6// fallback timer if idle timeout occurs
7const doTaskSync = () => {
8 console.log('Doing some work synchronously');
9 const timerId = setTimeout(() => {
10 doTask();
11 clearTimeout(timerId);
12 }, 2000);
13};
14const scheduleOnIdleTask = () => {
15 console.log('Scheduling task');
16 // state encapsulated using closures to avoid global variables
17 let id = null;
18
19 const cancelIdleTimer = () => {
20 if (id === null) return;
21 cancelIdleCallback(id);
22 id = null;
23 }
24
25 // Create the idle callback handler
26 const handleIdle = deadline => {
27 const remainingTime = deadline.timeRemaining();
28
29 if (deadline.didTimeout) {
30 // Timeout occurred
31 console.log('Timeout occurred, executing sync task');
32 cancelIdleTimer();
33 doTaskSync(); // Execute sync task
34 return;
35 }
36
37 if (remainingTime > 20) {
38 // Enough idle time
39 console.log('Enough idle time, executing task', remainingTime);
40 doTask();
41 return;
42 }
43 };
44
45 return {
46 cancel: cancelIdleTimer,
47 start: () => {
48 id = requestIdleCallback(handleIdle, { timeout: 2000 });
49 },
50 };
51};
52
53// Usage
54const idleHandler = scheduleOnIdleTask();
55idleHandler.start();
1Output:
2
3Scheduling task
4Not enough idle time 0.8
timeRemaining()
returns the remaining time in the current idle period. This value is a number between 0
and 50
, representing the number of milliseconds remaining in the current idle period.
If your callback function takes longer than the remaining time, it will block the main thread and delay the next frame rendering.
didTimeout
will be set to true
if the callback was invoked after the timeout period. ( Browser could not find a idle period within the specified time )
cancelIdleCallback()
disables the requestIdleCallback
callback.
timeRemaining()
provides an estimate, not an exact measurement, of how much time is left in the current idle period. The actual available time can fluctuate based on browser implementation and system conditions.
Additionally, the value returned by timeRemaining()
is intentionally capped at 50ms
. This limit ensures the browser can stay responsive to user interactions that may happen shortly after the idle callback starts.
1const scheduleOnIdleTask = () => {
2 if (!('requestIdleCallback' in window)) {
3 // Fallback for browsers that do not support requestIdleCallback
4 console.log('requestIdleCallback not supported, using fallback');
5 return {
6 start: doTaskSync,
7 cancel: () => {},
8 };
9 }
10
11 // else,
12 // ...requestIdleCallback implementation
13}
requestIdleCallback
is not supported in all browsers. You can use a polyfill or a fallback mechanism to ensure that your code works in all browsers.
See the compatibility table for more details.
If you expect your task to take longer than the remaining time in the current idle period, you can reschedule it to run in the next idle period.
1const rescheduleOnIdleTask = (callbackFn, options, id) => {
2 if (id !== null) {
3 cancelIdleCallback(id);
4 id = null;
5 }
6
7 return requestIdleCallback(callbackFn, options);
8};
Remember to clear your previous requestIdleCallback
using cancelIdleCallback()
before creating a new one.
1// Create the idle callback handler
2const handleIdle = deadline => {
3 const remainingTime = deadline.timeRemaining();
4
5 if (deadline.didTimeout) {
6 // same as previous example
7 // ...
8 return;
9 }
10
11 if (remainingTime > 20) {
12 // same as previous example
13 // ...
14 return
15 }
16
17 // Not enough idle time, reschedule
18 console.log('Not enough idle time, rescheduling', remainingTime);
19 id = rescheduleOnIdleTask(handleIdle, { timeout: 2000 });
20};
You can use the rescheduleIdle()
function to schedule your task to run in the next idle period. This goes inside the handleIdle()
function, after the if (remainingTime > 20) {
check
1// function that needs to be executed when the browser is idle
2const doTask = () => {
3 console.log('Doing some work');
4};
5
6// fallback timer if idle timeout occurs
7const doTaskSync = () => {
8 console.log('Doing some work synchronously');
9 const timerId = setTimeout(() => {
10 doTask();
11 clearTimeout(timerId);
12 }, 2000);
13};
14
15const rescheduleOnIdleTask = (callbackFn, options, id) => {
16 if (id !== null) {
17 cancelIdleCallback(id);
18 id = null;
19 }
20
21 return requestIdleCallback(callbackFn, options);
22};
23
24
25const scheduleOnIdleTask = () => {
26 if (!('requestIdleCallback' in window)) {
27 // Fallback for browsers that do not support requestIdleCallback
28 console.log('requestIdleCallback not supported, using fallback');
29 return {
30 start: doTaskSync,
31 cancel: () => {},
32 };
33 }
34
35 console.log('Scheduling task');
36 // state encapsulated using closures to avoid global variables
37 let id = null;
38
39 const cancelIdleTimer = () => {
40 if (id === null) return;
41 cancelIdleCallback(id);
42 id = null;
43 }
44
45 // Create the idle callback handler
46 const handleIdle = deadline => {
47 const remainingTime = deadline.timeRemaining();
48
49 if (deadline.didTimeout) {
50 // Timeout occurred
51 console.log('Timeout occurred, executing sync task');
52 cancelIdleTimer();
53 doTaskSync(); // Execute sync task
54 return;
55 }
56
57 if (remainingTime > 20) {
58 // Enough idle time
59 console.log('Enough idle time, executing task', remainingTime);
60 doTask();
61 return;
62 }
63
64 // Not enough idle time, reschedule
65 console.log('Not enough idle time, rescheduling', remainingTime);
66 id = rescheduleOnIdleTask(handleIdle, { timeout: 2000 });
67 };
68
69 return {
70 cancel: cancelIdleTimer,
71 start: () => {
72 id = requestIdleCallback(handleIdle, { timeout: 2000 });
73 },
74 };
75};
76
77// Usage
78const idleHandler = scheduleOnIdleTask();
79idleHandler.start();
1Output:
2
3Scheduling task
4Not enough idle time, rescheduling 6
5Not enough idle time, rescheduling 8
6Not enough idle time, rescheduling 8.1
7Enough idle time, executing task 48.8
8Doing some work
If your code takes longer than thetimeRemaining()
value, it will block the main thread and delay the next frame from rendering. This defeats the purpose of using requestIdleCallback
in the first place.
If the browser fails to find an idle period within the specified timeout, the didTimeout
property will be set to true
and the timeRemaining()
value will be 0
.
Fallback to a different scheduling mechanism or reschedule, based on your application needs.
1
2const handleIdle = deadline => {
3 const remainingTime = deadline.timeRemaining();
4
5 if (deadline.didTimeout) {
6 // fallback to async
7 return;
8 }
9
10 while (events.length > 0 && deadline.timeRemaining() >= 5) {
11 console.log('Scheduling task:', events[0]);
12 fireGaEvent(events.shift());
13 }
14
15 // no more events to process
16 if (events.length === 0) {
17 // cancel scheduler,
18 // create a new scheduler when events start flowing again
19 cancelIdleTimer();
20 }
21
22 // Not enough idle time, reschedule
23 console.log('Not enough idle time, rescheduling', remainingTime);
24 id = rescheduleOnIdleTask(handleIdle, { timeout: 2000 });
25};
Use requestIdleCallback
to send analytics data or tracking information when the browser is idle. This ensures that analytics tasks do not interfere with the main thread and do not block rendering.
requestIdleCallback
is a powerful tool for optimizing background tasks in JavaScript. By understanding how to use it effectively, you can improve the performance of your web applications and provide a smoother user experience.
Make sure you are implement workarounds for unsupported browsers and handle common pitfalls.