Before the order
- Does requestAnimationFrame really understand?
Happy New Year, everyone. As we start the New Year, let’s take a look at one of the most common, important, but not always used knowledge points: requestIdleCallback
This article will solve your questions:
- What exactly is requestIdleCallback?
- How does requestIdleCallback relate to the React Fiber architecture?
- How does requestIdleCallback help with performance tuning?
If this article is helpful to you, please add the author mokinzhao on wechat and enter the group for in-depth communication
What is requestIdleCallback?
RequestIdleCallback is an experimental API that lets you do things when your browser is idle
RequestIdleCallback Simply says that if a frame has free time, perform a task. The purpose is to solve the page frame loss (stuck) situation when tasks need to occupy the main process for a long time, resulting in higher priority tasks (such as animation or event tasks), unable to respond in time. Therefore, RequestIdleCallback localization deals with tasks that are not important or urgent.
The basic grammar
var handle = window.requestIdleCallback(callback[, options])
Copy the code
- The return value
An ID, you can put it into the Window. The cancelIdleCallback () method to end a callback.
- callback
A reference to a function to be called when the event loop is idle. The function receives a parameter called IdleDeadline, which gets the status of the current idle time and whether the callback has been executed before the timeout.
- The options of the optional
Includes optional configuration parameters. Has the following properties: TIMEOUT: If timeout is specified and there is a positive value, and the callback has not been called after Timeout milliseconds, the callback task will be queued in an event loop, even if doing so may have a negative impact on performance.
- The basic application
type Deadline = {
timeRemaining: () = > number // The current available time. That's the remaining time of the frame.
didTimeout: boolean // Whether timeout occurs.
}
function work(deadline:Deadline) { // Deadline has a method called timeRemaining() to retrieve the remaining free time of the current browser,Unit ms; There's a property didTimeout that says whether or not you're going to time outconsole.log('Remaining time of current frame:${deadline.timeRemaining()}`);
if (deadline.timeRemaining() > 1 || deadline.didTimeout) {
// If we have enough time, we can write our own logic here
}
// Run out of time, give control to the main thread, the next idle call
requestIdleCallback(work);
}
requestIdleCallback(work, { timeout: 1000 }); // You can pass a callback function (mandatory) and arguments (currently only timeout).
Copy the code
disadvantages
- This is an experimental feature
Some browsers are still developing this feature, please refer to the browser compatibility table to see which prefixes are appropriate for different browsers. The syntax and behavior of this feature may change in future versions of browsers as the standard documentation for this feature may be revised.
- The experimental process
Conclusion: The FPS of requestIdleCallback is only 20ms, and the normal rendering time is controlled at 16.67ms (1s / 60 = 16.67ms). The time is higher than the page fluency appeals.
- Some people think that
RequestIdleCallback is mainly used for unimportant and non-urgent tasks because React renders content that is not unimportant and non-urgent. The React team implemented the API on their own because it was only moderately compatible with frame rendering and didn’t quite meet the requirements of rendering
Difference between requestIdleCallback and requestAnimationFrame
The requestAnimationFrame callback is confirmed every frame and is a high priority task. The callback to requestIdleCallback is not necessarily a low-priority task. The page we see is drawn frame-by-frame by the browser, and FPS is usually smooth at 60, while FPS is slow at low FPS. So what does the browser do in each frame, as shown below:
One frame includes user interaction, JavaScript script execution; ***requestAnimationFrame(rAF)*** calls, layout calculations, page redrawing etc. If a frame does not perform a lot of tasks and the above tasks are completed in less than 16.66ms(1000/60), then the frame will have some free time to execute the requestIdleCallback callback, as shown in the figure below:
When the application stack is empty and the browser is idle, the time allowed for requestIdleCallback execution can be appropriately extended, up to 50ms, to prevent unpredictable tasks (such as user input) and avoid delays perceived by users when they cannot respond in a timely manner.
Since requestIdleCallback takes advantage of the frame’s idle time, it is possible that the browser is always busy and the callback cannot be executed. In this case, you need to pass the second configuration parameter timeout when calling requestIdleCallback.
requestIdleCallback(myNonEssentialWork, { timeout: 2000 });
function myNonEssentialWork(deadline) {
Deadline. didTimeout is true if the callback is executed because of a timeout
while (
(deadline.timeRemaining() > 0 || deadline.didTimeout) &&
tasks.length > 0
) {
doWorkIfNeeded();
}
if (tasks.length > 0) { requestIdleCallback(myNonEssentialWork); }}Copy the code
If the timeout callback is executed, the user may feel stuck because a frame is longer than 16ms.
RequestIdleCallback what you can and can’t do in your free time
What not to Do
- Do not perform DOM modification operations in requestIdleCallback
It is strongly recommended not to perform DOM modification operations in requestIdleCallback. As you can see from the composition of the above frame, the style changes, layout calculations, and so on are completed before the requestIdleCallback callback is executed. If you modify the DOM in the callback, the previous layout calculation is invalidated. And if there are operations related to retrieving the layout in the next frame, the browser will have to force a rearrangement, which can have a significant impact on performance. In addition, because the time to modify the DOM is unpredictable, it is easy to exceed the current frame free threshold.
The recommended approach is to make DOM changes in the requestAnimationFrame.
- In addition to DOM modification, **Promise’s resolve(reject)** is also not recommended because Promise’s callback executes immediately after Idle’s callback completes. The promise callback is a high-priority microtask, so it will be executed immediately after the requestIdleCallback callback ends, potentially putting a timeout on this frame.
What can do
- Data analysis and reporting
- Conduct data analysis and report when users have operation behaviors (such as clicking buttons and scrolling pages).
- Json.stringify is often called when processing data, which can cause performance problems if the data volume is large.
At this point, we can use requestIdleCallback to schedule reporting to avoid blocking page rendering. Here is a simple code example (skip)
const queues = [];
const btns = btns.forEach(btn= > {
btn.addEventListener('click'.e= > {
// do something
pushQueue({
type: 'click'
// ...
}));
schedule(); // Wait until idle
});
});
function schedule() {
requestIdleCallback(deadline= > {
while (deadline.timeRemaining() > 1) {
const data = queues.pop();
// Here you can process data and upload data
}
if (queues.length) schedule();
});
}
Copy the code
- preload
To load something in your spare time, take a look at an example from Qiankun that preloads JS and CSS
function prefetch(entry: Entry, opts? : ImportEntryOpts) :void {
if(! navigator.onLine || isSlowNetwork) {// Don't prefetch if in a slow network or offline
return;
}
requestIdleCallback(async() = > {const { getExternalScripts, getExternalStyleSheets } = await importEntry(entry, opts);
requestIdleCallback(getExternalStyleSheets);
requestIdleCallback(getExternalScripts);
});
}
Copy the code
- Detection of caton
- Generally, there are two kinds of stuck detection methods:
- Measure the FPS value. If several consecutive FPS values are less than or equal to the threshold, it is considered to be stuck
- Open a heartbeat detection between a worker thread and the main thread. If there is no response within a period of time, it is considered to be stuck
In retrospect, if a requestIdleCallback is not executed for a long period of time, there is no free time and it is most likely that something is stuck and can be reported. It is better for behavior stalling, for example: clicking on a button and adding our requestIdleCallback callback, if the callback is not executed for some time after the click, there is a high probability that the click caused the stalling.
- Splitting time-consuming Tasks
This idea is vividly shown in the Scheduler Scheduler in React. Although React implements a set of scheduling logic (compatibility, stability, priority, etc.), it does not hinder our understanding. React transforms the diff process from recursion to iteration. Recursive diff on two large objects is a time-consuming task. It would be nice if we could break it down into smaller tasks. However, recursion can’t end in the middle, so React uses a data structure like Fiber, which turns recursion into a linked list iteration. The iteration can stop in the middle, so we don’t have to diff all at once. Ps: don’t know the list of students is simple to understand as an array, you think if we want to think about array traversal, we can finish one-time execution, but we can also split into several times after, as long as we record the index, next time come back to continue executing code is from the index began to traverse line, don’t know you get to the no.
Analog implementation of requestIdleCallback
Use setTimeout implementation
First, we need to know why setTimeout can be used to simulate, so let’s take a look at the following two lines of code:
// To some extent, the function is similar, and the writing style is similar
requestIdleCallback(() = > console.log(1));
setTimeout(() = > console.log(2));
Copy the code
If you know about setTimeout, you should know that it is not correct. It does not mean to execute immediately, but to execute as fast as possible, that is, wait until the main thread is empty and the microtask is finished, then it is the turn of setTimeout to execute. SetTimeout (fn) has an extra parameter in it:
window.requestIdleCallback = function(cb) {
let start = Date.now();
return setTimeout(function () {
const deadline = { // This is to construct parameters
timeRemaining: () = > Math.max(0.50 - (Date.now() - start)), // We write the remaining time within 50ms, which is the upper limit mentioned above. In fact, you can also write 40, 30, 16, 10, etc 😂
didTimeout: false // Since we do not recommend using the timeout argument, we will simply write false here
};
cb(deadline);
});
}
Copy the code
Note that this is not a polyfill of requestIdleCallback, because they are not actually the same. SetTimeout is not really using idle time, but executing your code as fast as conditions allow. The code above doesn’t limit itself to the free time of this frame like the real requestIdleCallback does, but it does two things: one is to segment the task and the other is to control the upper limit of time for each execution. Macros are generally the only tasks that satisfy these two conditions, so in addition to setTimout, postMessage is also possible. Now let’s look at another way to simulate
RequestAnimationFrame + MessageChannel
let deadlineTime // End time of the current frame
let callback // The task that needs to be called back
let channel = new MessageChannel(); // A type of postMessage that has two and only ports and can send and receive events to and from each other.
let port1 = channel.port1;
let port2 = channel.port2;
port2.onmessage = () = > {
const timeRemaining = () = > deadlineTime - performance.now();
if (timeRemaining() > 1 && callback) {
const deadline = { timeRemaining, didTimeout: false }; // Also construct a parameter herecallback(deadline); }}window.requestIdleCallback = function(cb) {
requestAnimationFrame(rafStartTime= > {
// Approximate expiration time = default this is the start time of a frame + approximate time of a frame
deadlineTime = rafStartTime + 16
callback = cb
port1.postMessage(null);
});
}
Copy the code
This approach is slightly better than setTimeout because MessageChannel executes before setTimeout and does not have a minimum delay of 4ms. So why not use micromission simulation? Because if you use microtask emulation, after the code is finished, all the microtasks will continue to execute completely, and the main thread cannot be abandoned in time.
- Ps: Neither method is polyfill, just as close to the requestIdleCallback as possible, and the remaining time is also guesswork.
How does requestIdleCallback relate to time slicing in React
The RequestIdleCallback experiment case
- Conclusion:
- RequestIdleCallback uses the idle time between frames to execute JS
- RequestIdleCallback comes after Layout and Paint, which means that requestIdleCallback can be js computed and DOM altered, which means that layout and Paint will be rearranged
- RequestAnimationFrame comes before Layout and Paint, so it’s better to change DOM operations.
- Hence the React internal implementation of the scheduling policy is also based on requestAnimationFrame
- Reason:
- RequestIdleCallback is positioned to handle unimportant and non-urgent things. Because React renders content, it’s not that it’s unimportant and not urgent. The React team implemented the API on their own because it was only moderately compatible with frame rendering and didn’t quite meet the requirements of rendering
React source code requestHostCallback
-
SchedulerHostConfig.js
-
Execute macro task (callback task)
- RequestHostCallback: Triggers a macro task performWorkUntilDeadline.
- PerformWorkUntilDeadline: macro task processing.
- Whether there is plenty of time, there is implementation.
- Check whether there is another callback task after this callback task is executed, that is, check hasMoreWork.
- PostMessage (null);
let scheduledHostCallback = null;
let isMessageLoopRunning = false;
const channel = new MessageChannel();
/ / sent port2
const port = channel.port2;
/ / received port1
channel.port1.onmessage = performWorkUntilDeadline;
const performWorkUntilDeadline = () = > {
// There is an execution task
if(scheduledHostCallback ! = =null) {
const currentTime = getCurrentTime();
// Yield after `yieldInterval` ms, regardless of where we are in the vsync
// cycle. This means there's always time remaining at the beginning of
// the message event.
// Calculate the expiration point of a frame
deadline = currentTime + yieldInterval;
const hasTimeRemaining = true;
try {
// After executing the callback, determine whether there are other tasks to follow
const hasMoreWork = scheduledHostCallback(
hasTimeRemaining,
currentTime,
);
if(! hasMoreWork) { isMessageLoopRunning =false;
scheduledHostCallback = null;
} else {
// If there's more work, schedule the next message event at the end
// of the preceding one.
// There are other tasks to push into the next macro task queue
port.postMessage(null); }}catch (error) {
// If a scheduler task throws, exit the current browser task so the
// error can be observed.
port.postMessage(null);
throwerror; }}else {
isMessageLoopRunning = false;
}
// Yielding to the browser will give it a chance to paint, so we can
// reset this.
needsPaint = false;
};
// requestHostCallback executes tasks in a frame
requestHostCallback = function(callback) {
// Callback registration
scheduledHostCallback = callback;
if(! isMessageLoopRunning) { isMessageLoopRunning =true;
// Enter the macro task queue
port.postMessage(null); }}; cancelHostCallback =function() {
scheduledHostCallback = null;
};
Copy the code
The author established a full stack large front-end communication group, like to discuss hot technology, please add wechat: Mokinzhao
- Front-end technology summary: full stack big front-end column
reference
requestIdleCallback-MDN
Take a closer look at the requestIdleCallback
React requestIdleCallback scheduler