7

1. What is requestAnimationFrame

window.requestAnimationFrame() tells the browser-you want to execute an animation, and ask the browser to call the specified callback function to update the animation before the next redraw. This method needs to pass in a callback function as a parameter, and the callback function will be executed before the next browser redraw.
According to the definition of MDN requestAnimationFrame is an API provided by the browser to redraw web pages by frame. Take a look at the following example first to understand how it is used and run:
const test = document.querySelector<HTMLDivElement>("#test")!;
let i = 0;
function animation() {
  if (i > 200) return;
  test.style.marginLeft = `${i}px`;
  window.requestAnimationFrame(animation);
  i++;
}
window.requestAnimationFrame(animation);

The above code 1s is executed about 60 times, because the refresh frequency of general screen hardware devices is 60Hz , and then about 16.6ms every time it is executed. When using requestAnimationFrame , you only need to call it repeatedly to achieve animation effects.

At the same time, requestAnimationFrame will return a request ID, which is a unique value in the callback function list. You can use cancelAnimationFrame to cancel the callback function by passing in the request ID.

const test = document.querySelector<HTMLDivElement>("#test")!;
let i = 0;
let requestId: number;
function animation() {
  test.style.marginLeft = `${i}px`;
  requestId = requestAnimationFrame(animation);
  i++;
  if (i > 200) {
    cancelAnimationFrame(requestId);
  }
}
animation();

Figure 1 below is the execution result of the above example:

raf动画

2. Confusion in the implementation of requestAnimationFrame

The way to use JavaScript to achieve animation can also use setTimeout , the following is the implementation code:

const test = document.querySelector<HTMLDivElement>("#test")!;
let i = 0;
let timerId: number;
function animation() {
  test.style.marginLeft = `${i}px`;
  // 执行间隔设置为 0,来模仿 requestAnimationFrame
  timerId = setTimeout(animation, 0);
  i++;
  if (i > 200) {
    clearTimeout(timerId);
  }
}
animation();

Here setTimeout is set to 0 to imitate requestAnimationFrame .

You can't see any difference from the way of code implementation alone, but from the specific implementation results below, you can see a clear gap.

Figure 2 below is the execution result setTimeout

st动画

For a complete example, stamp codesandbox .

It is obvious that setTimeout much "faster" than the animation realized by requestAnimationFrame What is the reason for this?

As you might have guessed, Event Loop and requestAnimationFrame have some special mechanisms during execution. Let’s explore the relationship between Event Loop and requestAnimationFrame

3. Event Loop and requestAnimationFrame

Event Loop (event loop) is a browser internal mechanism used to coordinate events, user interaction, scripting, rendering, and networking.

Event Loop is also divided into several types in the browser:

  • window event loop
  • worker event loop
  • worklet event loop

Our main discussion here is window event loop . Event Loop controlled by the main thread in a rendering process of the browser.

3.1 task queue

A Event Loop has one or more task queues . A task queue is a collection of a series of tasks.

Note: A task queue is a collection in the data structure, not a queue, because the event loop processing model will get the first runnable task from the selected task queue instead of making the first one The task is dequeued.
The above content comes from the HTML specification . What is confusing here is that it is obviously a collection, why is it called "queue" TT

3.2 task

A task can have multiple task sources (task sources), which task sources are there? Take a look at Gerneric task sources :

  • DOM manipulation task source, such as inserting an element into the document in a non-blocking manner
  • User interaction task source, user operation (such as click) event
  • Network task source, network I/O response callback
  • history traversal task source, such as history.back()

In addition there like Timers ( setTimeout , setInterval etc.), IndexDB operation is also task source .

3.3 microtask

A event loop has a microtask queue , but this "queue" is indeed the " FIFO " queue.

The specification does not specify which task sources are microtasks. Generally, the following are considered to be microtasks:

  • promises
  • MutationObserver
  • Object.observe
  • process.nextTick (This thing is the API of Node.js, so I won’t discuss it for now)

3.4 Event Loop Processing Process

  1. It is agreed that a runnable task must be included in the selected task queue (taskQueue). If there is no such task queue, skip to the microtasks step below.
  2. Make the oldest task (oldestTask) in taskQueue the first executable task, and then delete it from taskQueue.
  3. Set the oldestTask above as the running task in the event loop.
  4. Execute the oldestTask.
  5. Set the running task in the event loop to null.
  6. Execute microtasks checkpoints (that is, execute tasks in the microtasks queue).
  7. Set hasARenderingOpportunity to false.
  8. Update rendering.
  9. If it is currently window event loop and there is no task in the task queues and the microtask queue is empty, and the rendering timing variable hasARenderingOpportunity is false, execute the idle period ( requestIdleCallback ).
  10. Return to the first step.

The above is event loop from the specification, some contents are omitted, the full version is here .

Generally speaking, event loop is to constantly look for executable tasks in the task queues. If there is, push it to call stack (execution stack) for execution, and update the rendering at the right time.

Figure 3 below ( source ) is event loop running on the main thread of the browser:

主线程 event loop

As for what the main thread does, this is another grand topic. Interested students can check out the browser internal secret series article .

In the above specification, the rendering process is after the execution of the microtasks queue, and then take a look at the rendering process.

3.5 update rendering

  1. To traverse all documents in the current browsing context, each document must be processed in the order found in the list.
  2. Rendering opportunities (Rendering opportunities) : If there is no rendering opportunity in the current browsing context, delete all docs and cancel rendering (whether there is a rendering opportunity here is determined by the browser itself, according to hardware refresh rate restrictions, page performance or whether the page is Factors in the background).
  3. If the current document is not empty, set hasARenderingOpportunity to true.
  4. Unnecessary rendering: If the browser thinks that the rendering of the browsing context of the updated document will not produce a visible effect and the animation frame callbacks of the document are empty, the rendering is canceled. (Finally saw the figure of requestAnimationFrame
  5. Remove the docs from the docs that the browser thinks is best to skip updating the rendered docs for other reasons.
  6. If the browsing context of the document is the top-level browsing context, refresh the auto-focus candidates of the document.
  7. To handle the resize event, pass in a performance.now() timestamp.
  8. To handle the scroll event, pass in a performance.now() timestamp.
  9. To process media queries, pass in a performance.now() timestamp.
  10. To run the CSS animation, pass in a performance.now() timestamp.
  11. To handle full screen events, pass in a performance.now() timestamp.
  12. executes the requestAnimationFrame callback and passes in a performance.now() timestamp .
  13. Execute the intersectionObserver callback and pass in a performance.now() timestamp.
  14. Draw each document.
  15. Update the ui and present it.

Figure 4 below ( source ) is a relatively clear flow of this process:

life of a frame

At this point, the requestAnimationFrame of the callback of 0618cf0bd25768 is clear, and it will be style/layout/paint before 0618cf0bd2576a.

setTimeout requestAnimationFrame animation is faster than the 0618cf0bd25781 animation mentioned at the beginning of the article, this is a good explanation.

First of all, the browser rendering has a rendering opportunity problem, that is, the browser will judge whether to render according to the current browsing context. It will try to be as efficient as possible and only render when necessary. If there is no interface change will not be rendered. According to the specification, because of hardware refresh frequency limitations, page performance, and whether the page has a background, etc., it is possible setTimeout has been executed, it is found that the rendering time has not yet arrived, so setTimeout back several times. Before rendering, the difference between the marginLeft set at this time and the marginLeft before the last rendering should be greater than 1px.

Figure 5 setTimeout execution of 0618cf0bd257cb. The red circle shows two renderings, and the middle four times are for processing setTimout task . Because the screen refresh rate is 60 Hz, so it is approximately 16.6ms after the execution of setTimeout task for multiple times before the rendering time came. Perform rendering.

两次渲染之间大概执行了4次

requestAnimationFrame difference between the 0618cf0bd257f3 frame animation is that it will be called before each rendering. The difference between the marginLeft set at this time and the marginLeft before the last rendering is 1px.

Figure 6 requestAnimationFrame execution of 0618cf0bd25804. Rendering will be performed after each call:

每次调用raf均渲染

调用渲染

So it seems that setTimeout "faster".

4. Implementation of different browsers

The above example is in Chrome under test, the results of this basic example presented in all browsers are the same, look at the following example, it comes from Jake archilbald earlier proposed in 2017 this problem :

test.style.transform = 'translate(0, 0)';
document.querySelector('button').addEventListener('click', () => {
  const test = document.querySelector('.test');
  test.style.transform = 'translate(400px, 0)';
  
  requestAnimationFrame(() => {
    test.style.transition = 'transform 3s linear';
    test.style.transform = 'translate(200px, 0)';
  });
});

The execution of this code in Chrome and Firefox is shown in Figure 7:

chrome

To explain briefly, in this example, requestAnimationFrame overrides the transform set in the click listener, because requestAnimationFrame is called before the css (style) is calculated, so the animation moves 200 px to the right.

Note: The above code is executed in Chrome hidden mode. When your Chrome browser has many plug-ins or opens many tabs, it may also slide from right to left.
The implementation in safari is as shown in Figure 8:

safari

Edge also had the same execution result as safari before, but it has been fixed now.

The reason for this result is that safari executes the requestAnimationFrame callback at 1 frame after rendering, so the requestAnimationFrame called in the current frame will render in the next frame. So safari first rendered the position to the right 400px position, and then moved to the left 200px position.

Regarding a more detailed explanation of the execution mechanism of event loop and requestAnimationFrame special lecture , I recommend you to take a look.

5. Other enforcement rules

Continuing to look at the example that jake put forward, if under the implementation of the standard specification, what do you need to do if you want to achieve the effect presented by safari (that is, move from right to left)?

The answer is to add another layer of requestAnimationFrame call:

test.style.transform = 'translate(0, 0)';
document.querySelector('button').addEventListener('click', () => {
  const test = document.querySelector('.test');
  test.style.transform = 'translate(400px, 0)';
  
  requestAnimationFrame(() => {
    requestAnimationFrame(() => {
      test.style.transition = 'transform 3s linear';
      test.style.transform = 'translate(200px, 0)';
    });
  });
});

The execution result of the above code is consistent with safari, because requestAnimationFrame is executed only once per frame, and the newly defined requestAnimationFrame will be executed before the next frame is rendered.

6. Other applications

From the above example, we know that the use of setTimeout to perform visual changes such as animation is likely to cause frame loss and setTimeout . Therefore, you should try to avoid using 0618cf0bd25a24 to perform animation. It is recommended to use requestAnimationFrame to replace it.

requestAnimationFrame only used to achieve animation effects, but also can be used to implement split execution of large tasks.

From the rendering flowchart in Figure 4, we can know that: executes JavaScript task before rendering. If JavaScript executes for too long within a frame, rendering will be blocked, which will also cause frame .

In response to this situation, the JavaScript task can be divided into small blocks, and use requestAnimationFrame() to run on each frame. As shown in the following example ( source ):

var taskList = breakBigTaskIntoMicroTasks(monsterTaskList);
requestAnimationFrame(processTaskList);
function processTaskList(taskStartTime) {
  var taskFinishTime;
  do {
    // 假设下一个任务被压入 call stack
    var nextTask = taskList.pop();
    // 执行下一个 task
    processTask(nextTask);
    // 如何时间足够继续执行下一个
    taskFinishTime = window.performance.now();
  } while (taskFinishTime - taskStartTime < 3);
  if (taskList.length > 0) {
    requestAnimationFrame(processTaskList);
  }
}

7. Reference materials

WHATWG HTML Standard

internal secrets of modern browsers

JavaScript main thread. Dissected.

requestAnimationFrame Scheduling For Nerds

jake jsconf speech

optimize javascript execution

Explore the timing of javaScript asynchronous and browser update rendering from the event loop specification


Welcome to follow the blog of Lab: 1618cf0bd25b9b aotu.io

Or follow the AOTULabs official account (AOTULabs) and push articles from time to time.


凹凸实验室
2.3k 声望5.5k 粉丝

凹凸实验室(Aotu.io,英文简称O2) 始建于2015年10月,是一个年轻基情的技术团队。