1. What is requestAnimationFramepda

Tell the browser window. RequestAnimationFrame () – you want to perform an animation, and required the browser until the next redraw calls the specified callback function to update the animation. This method takes as an argument a callback function that is executed before the browser’s next redraw.

RequestAnimationFrame is a browser-provided API for redrawing a web page frame according to the MDN definition above. Take a look at the following example to see how it works in action:

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);
Copy the code

The above code is executed about 60 times, since the average screen hardware device is refreshed at 60Hz, and then about 16.6ms per execution. When you use requestAnimationFrame, you just need to call it repeatedly to animate.

RequestAnimationFrame also returns a request ID, which is a unique value in the list of callback functions. CancelAnimationFrame cancels 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();
Copy the code

Figure 1 shows the result of the above example:

2. Confusion in requestAnimationFrame execution

You can also use setTimeout to animate using JavaScript. Here is the code:

const test = document.querySelector<HTMLDivElement>("#test")! ;let i = 0;
let timerId: number;
function animation() {
  test.style.marginLeft = `${i}px`;
  // Set the execution interval to 0 to mimic requestAnimationFrame
  timerId = setTimeout(animation, 0);
  i++;
  if (i > 200) {
    clearTimeout(timerId);
  }
}
animation();
Copy the code

Set setTimeout to 0 here to mimic requestAnimationFrame.

Just looking at the way the code is implemented, you can’t tell the difference, but you can see the difference in the concrete implementation results below.

Figure 2 shows the setTimeout result:

For the full example, poke Codesandbox.

Obviously, setTimeout is “faster” than the requestAnimationFrame implementation. What is the reason for this?

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

3. The Event Loop and requestAnimationFrame

Event Loop is an internal browser mechanism for coordinating events, user interactions, scripting, rendering, and networking.

There are also several Event loops in the browser:

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

The main thing we’re talking about here is the Window Event loop. This is the Event Loop controlled by the main thread of a browser render process.

3.1 task queue

An Event Loop has one or more Task queues. A Task queue is a collection of tasks.

Note: A Task queue is structurally a collection, not a queue, because the event loop model retrieves the first runnable task from the selected task queue rather than making the first task out of the queue.

The above is from the HTML specification. The puzzle here is why it’s called a queue when it’s a collection

3.2 task

A task can have multiple task sources. What are the task sources? Gerneric Task Sources

  • DOM manipulates task sources, such as inserting an element into a document in a non-blocking manner
  • User interaction task sources, user action (such as click) events
  • Network task source, network I/O response callback
  • History Traversal task source, such as history.back()

In addition, other operations such as Timers (setTimeout, setInterval, etc.) and IndexDB are also task sources.

3.3 microtask

An event loop has a microTask queue, but the “queue” is really the queue for the “FIFO”.

The specification does not specify which microTask’s task sources are, and the following are generally considered microTasks:

  • promises
  • MutationObserver
  • Object.observe
  • Process.nexttick (this is the API of Node.js and will not be discussed for the moment)

3.4 Event Loop Process

  1. The convention must include a runnable task in the selected Task Queue (taskQueue). If there is no such task Queue, go to the microTasks step below.
  2. Make the oldestTask (oldestTask) in the taskQueue the first executable task, and then delete it from the taskQueue.
  3. Set the oldestTask above to the task running in the Event loop.
  4. Perform oldestTask.
  5. Set the running task in the Event loop to NULL.
  6. Perform microTasks checkpoints (that is, execute tasks in the MicroTasks queue).
  7. Set hasARenderingOpportunity to false.
  8. Update render.
  9. If the current iswindow event loopThe queues are empty and the Task queues have no tasks. The microtask queue is empty. The hasARenderingOpportunity variable is false.requestIdleCallback).
  10. Go back to step 1.

The above is a condensed version of the event loop processing from the specification, with some omitted content. The full version is here.

Basically, an Event loop is simply a queue that keeps finding out if there are executable tasks in Task Queues, pushing them to the Call Stack for execution if they exist, and updating the render when appropriate.

Figure 3 (source) shows a clear flow of the Event Loop running on the main thread of the browser:

What the main thread does, again, is a big topic, and for those of you who are interested, check out the Inside of the browser series.

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

3.5 Update the rendering

  1. Iterating through all the documents in the current browsing context, each document must be processed in the order found in the list.
  2. Rendering Opportunities: Delete all docs and cancel Rendering if there is no Rendering opportunity in the current browsing context (it is up to the browser to determine whether Rendering opportunities exist, depending on hardware refresh rate limits, page performance, and whether the page is in the background).
  3. If the current document is not empty, set hasARenderingOpportunity to true.
  4. Unnecessary Rendering: If the browser considers that the rendering of the updated document’s browsing context will not be visible and the document’s Animation Frame Callbacks are empty, cancel rendering. Finally see the requestAnimationFrame
  5. Remove documents from Docs that the browser thinks are best to skip updating rendered for other reasons.
  6. If the browse context of a document is a top-level browse context, refresh the autofocus candidate for that document.
  7. To handle the resize event, pass in a performing.now () timestamp.
  8. Handle the Scroll event, passing in a performance.now() timestamp.
  9. Process the media query, passing in a performing.now () timestamp.
  10. Run the CSS animation, passing in a perform.now () timestamp.
  11. To handle a full-screen event, pass in a performing.now () timestamp.
  12. Execute the requestAnimationFrame callback, passing in a performing.now () timestamp.
  13. A performance.now() timestamp is passed in by executing the intersectionObserver callback.
  14. Draw each document.
  15. Update the UI and render.

Figure 4 (source) shows a clear flow of this process:

At this point, the timing of the requestAnimationFrame callback is clear, and it will be called before Style/Layout/Paint.

Coming back to the setTimeout animation being faster than the requestAnimationFrame animation mentioned at the beginning of the article, this is easy to explain.

First, the browser has an issue of Rendering opportunity, which is to determine whether or not to render based on the current viewing context. It will render as efficiently as possible, only when necessary, and will not render if there is no interface change. As stated in the specification, due to the limitation of hardware refresh frequency, page performance and whether the page has background and other factors, it is possible to find that it is not the time to render after executing the setTimeout task, so setTimeout callback several times before rendering. The difference between the default marginLeft and the default marginLeft before the last rendering will be greater than 1px.

Figure 5 below is the execution of setTimeout. In the red circle, there are two renders, and the four in the middle are setTimout task processing, because the screen refresh frequency is 60 Hz. So it took approximately 16.6ms to execute the setTimeout task several times before it was time to render and execute the render.

RequestAnimationFrame frame animation is different in that it is called before each render, where the difference between the default marginLeft and the default marginLeft before the last render is 1px.

Figure 6 shows requestAnimationFrame execution, with rendering performed after each call:

So setTimeout looks a lot faster.

4. Implementation of different browsers

All of the above examples were tested in Chrome, and the results are almost consistent across all browsers. Take a look at this example from Jake Archilbald who asked this question back in 2017:

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)';
  });
});
Copy the code

This code is executed in Chrome and Firefox as shown in Figure 7 below:

For a quick explanation, the transform set in the requestAnimationFrame callback overrides the transform set in the Click Listener, Because requestAnimationFrame is called before calculating CSS (style), the animation moves 200 px to the right.

Note: This code is executed in Hidden Chrome mode, which can also slide from right to left if your Chrome browser has many plugins or many tabs open.

In Safari, it looks like figure 8:

Edge used to perform the same as Safari, but that has now been fixed.

The reason for this is that Safari executes the requestAnimationFrame callback after frame 1 has been rendered, so the requestAnimationFrame called by the current frame will be rendered in the next frame. So Safari starts rendering 400px to the right and then moves 200px to the left.

The event Loop and requestAnimationFrame execution mechanisms are explained in more detail in JsConf.

5. Other execution rules

Continuing with jake’s previous example, what do YOU do if you want to achieve safari’s rendering (that is, moving from right to left) under the standard specification?

The answer is to add another layer of requestAnimationFrame calls:

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)';
    });
  });
});
Copy the code

RequestAnimationFrame is executed only once per frame, and the newly defined requestAnimationFrame is executed before the next frame is rendered.

6. Other applications

From the above example we know that using setTimeout to perform visual changes such as animations is likely to result in frame loss and lag, so avoid using setTimeout to perform animations as much as possible and recommend using requestAnimationFrame instead.

RequestAnimationFrame can be used to implement large tasks apart from animations.

It can be seen from the rendering flow chart in Figure 4 that JavaScript task is executed before rendering. If JavaScript execution time is too long within a frame, rendering will be blocked, which will also result in frame loss and stutter.

In this case, you can break the JavaScript task into smaller chunks and run it on each frame using requestAnimationFrame(). See the following example (source) :

var taskList = breakBigTaskIntoMicroTasks(monsterTaskList);
requestAnimationFrame(processTaskList);
function processTaskList(taskStartTime) {
  var taskFinishTime;
  do {
    // Assume that the next task is pushed into the Call Stack
    var nextTask = taskList.pop();
    Execute the next task
    processTask(nextTask);
    // How long does it take to proceed to the next one
    taskFinishTime = window.performance.now();
  } while (taskFinishTime - taskStartTime < 3);
  if (taskList.length > 0) { requestAnimationFrame(processTaskList); }}Copy the code

7. Reference materials

WHATWG HTML Standard

Inside the modern browser

JavaScript main thread. Dissected.

requestAnimationFrame Scheduling For Nerds

Jake jsconf speech

optimize javascript execution

Explore javaScript asynchrony and browser update rendering timing from the Event Loop specification