Optimizing Background Tasks with requestIdleCallback:
Advanced Scheduling in the JavaScript Event Loop

Published on: May 15, 2025

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.

When is the browser idle?

Idle times are periods when the browser is not busy processing user input, rendering, or performing other tasks.

Short idle times

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...

Long idle times

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...

Basic usage

javascript
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();
text
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.

Compatibility with unsupported browsers

javascript
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.

Reschedule to find a longer idle period

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.

Creating the reschedule function

javascript
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.

Consuming the reschedule function

javascript
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

Putting it all together

javascript
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();
text
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

Common pitfalls

Respect the idle time

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.

Handle timeouts properly

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.

Real-world use cases

Analytics and tracking

javascript
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.

Conclusion

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.

In this article