If you have any questions or feedback, pleasefill out this form
Table of Contents
- What Does It Mean to Be Idle?
- Introduction to requestIdleCallback
- Can requestIdleCallback Be Interrupted?
- What Happens When You Perform DOM Operations Inside requestIdleCallback?
- What Happens When You Call requestIdleCallback Inside requestIdleCallback?
- Example, Please!
- How About React?
- A Few Questions
- Conclusion
This post is translated by ChatGPT and originally written in Mandarin, so there may be some inaccuracies or mistakes.
Many web pages contain various scripts
that need to be executed, and naturally, there are priorities involved. High-priority tasks include rendering the UI, registering relevant interactive events, and calling APIs to fetch data. In contrast, lower-priority tasks might include analytics scripts, lazy loading, and initializing less important events.
What Does It Mean to Be Idle?
How can we determine when the browser is in an idle state? This is quite a complex issue, as the browser schedules a series of tasks: parsing HTML, CSS, and JavaScript, rendering the UI, making API calls, fetching images, parsing them, and GPU acceleration, among others. To know when the browser is idle, one must understand how the browser's scheduling works. Fortunately, requestIdleCallback
helps us address this issue.
Introduction to requestIdleCallback
requestIdleCallback
is executed at the end of a frame, but it is not guaranteed to run in every frame. The reason is simple: we cannot ensure that there will be time left when a frame ends, so the execution timing of requestIdleCallback
cannot be guaranteed.
Upon closer inspection, requestIdleCallback
feels somewhat like a context switch; you can complete some work between frames, take a break, and then continue execution.
requestIdleCallback(fn, {timeout})
const myWork = deadline => {
console.log("not important job.")
while (deadline.timeRemaining() > 0) {
// sending logging
// fetch non-essential data.
// use your imaginary
}
abortMyJob()
}
The second parameter, options, contains a timeout
option. If the browser has not called the callback within the timeout period, you can use this timeout to force the browser to stop its current work and call your function.
This is not an appropriate way to use it, as we utilize idle time precisely because the task is unimportant, and there is no need to interrupt ongoing work for it. The browser provides this flexibility, but sometimes you may still want an event to trigger within a certain timeframe.
cancelIdleCallback(id)
When you call requestIdleCallback()
, it returns an ID, which you can use to call cancelIdleCallback(id)
to cancel unnecessary idle callbacks.
Can requestIdleCallback Be Interrupted?
The deadline
parameter indicates how much time you have to complete the task within a frame. The callback receives this parameter, and according to official statements, even if you exceed this time, the browser will not forcibly interrupt your task; it simply hopes you can complete it by the deadline
to provide the best user experience.
deadline.timeRemaining()
returns the amount of time still available.
What Happens When You Perform DOM Operations Inside requestIdleCallback?
Consider that requestIdleCallback
runs at the end of a frame, meaning the browser has already completed recalculating, layout, and painting. If you modify the DOM at this point, it forces the browser to perform recalculating styles, layout, and painting again.
What Happens When You Call requestIdleCallback Inside requestIdleCallback?
Calling requestIdleCallback
within another requestIdleCallback
is valid. However, this callback will be scheduled for the next frame (though it may not necessarily be the next frame, depending on the browser's scheduling).
Example, Please!
Let's say we have several users, and when the mouse hovers over their profile picture, a personal intro appears. To make good use of idle time, we can fetch the required API data in requestIdleCallback
. If the data hasn't been fetched yet, we can call the API to retrieve it.
function fetchUser(name) {
const users = {
kalan: "food, coffee, life",
jack: "woman, coffee, life",
}
return Promise.resolve(users[name])
}
const userIntro = {}
const queue = [
{ name: "kalan", fetched: false },
{ name: "jack", fetched: false },
]
requestIdleCallback(deadline => {
while (deadline.timeRemaining() > 0) {
let q = queue.pop()
fetchUser(q.name).then(user => {
if (deadline.timeRemaining() > 0) {
userIntro[user.name] = user
q.fetched = true
}
})
}
}, 500)
avatar.addEventListener("mouseover", e => {
const name = e.target.getAttribute("data-name")
if (userIntro[name]) {
// show intro
} else {
fetchUser(name).then(user => showInfo(user))
}
})
In this example, we use requestIdleCallback
to pre-fetch user data and store it, enabling us to show it immediately when the user clicks on their profile picture. If the data hasn't been retrieved yet, we call fetchUser
again.
To demonstrate the effects of requestIdleCallback
, this example may seem a bit cumbersome. It requires maintaining a queue to check if data has already been fetched, and if a mouseover
event triggers, we need to check userIntro
for values as well. In development, it may be simpler to fetch data for multiple users at once, but this entirely depends on the requirements and use cases.
Analytics
Another common scenario is tracking, such as monitoring user clicks on buttons, play actions, or viewing times. We can collect events first and then send them all at once using requestIdleCallback
. For example:
const btns = btns.forEach(btn => // buttons you want to track.
btn.addEventListener('click', e => {
// do other interactions...
//...
putIntoQueue({
type: 'click'
// collect your data
}));
schedule();
});
function schedule() {
requestIdleCallback(
deadline => {
while (deadline > 0) {
const event = queues.pop();
send(event);
}
},
{ timeout: 1000 }
);
}
Here, we added a timeout
to ensure the browser calls the schedule
function.
How About React?
To prevent other unnecessary tasks from blocking the main thread, we can integrate React hooks to encapsulate all non-essential work inside useIdleCallback
.
import { useEffect, useRef } from "react"
function useIdleCallback(callback, timeout) {
useEffect(() => {
let id = requestIdleCallback(deadline => {
callback(deadline)
}, timeout)
return () => cancelIdleCallback(id)
}, [callback, timeout])
}
function UserIntro({ data }) {
useIdleCallback(() => {
sendLog(data)
})
return // your awesome UI
}
A Few Questions
The examples above are just for demonstration purposes; there are many real-world issues to handle, such as managing the queue, handling timeout
, and even marking your tasks with priorities to ensure execution order, all of which can be optimized.
Conclusion
Browser support for requestIdleCallback
is still not very good:
In React's Fiber, a similar mechanism is actually utilized. Previously, during work execution, the main thread could become blocked due to a deep call stack or too many updates in the updateQueue
, leading to prolonged update times. Fiber's mechanism breaks work into smaller chunks, so if there are higher-priority tasks, they can be executed first, and then the processing of other tasks can resume.
If you found this article helpful, please consider buying me a coffee ☕ It'll make my ordinary day shine ✨
☕Buy me a coffee