In depth analysis of event loop and browser rendering, frame animation, idle callback (animation demonstration)

Time:2020-9-24

preface

There are a lot of articles on event loop, but many of them are just about “macro task” and “micro task”. I will first ask a few questions:

  1. Is every round of event loop accompanied by rendering?
  2. requestAnimationFrameAt which stage, before or after rendering? staymicroTaskBefore or after?
  3. requestIdleCallbackAt what stage? How to implement it? Before or after rendering? staymicroTaskBefore or after?
  4. resizescrollWhen were the incidents distributed.

These problems are not intended to create difficulties for you. If you don’t know these problems, you may not be able to make a reasonable choice when encountering an animation demandrequestAnimationFrameYou may have thought of it when doing some requirementsrequestIdleCallbackBut you don’t know when it will run, just use it in fear and pray not to bug the line.

This is also one of the motivations of this paper to start with normative interpretation and dig deep into the bottom. This paper will exclude some obscure concepts from the specification, or not related to the main process. A more detailed version can also read the specification directly, but it is time-consuming and laborious.

event loop

We start with the event cycle of the browser according to the HTML official specification, because the rest of the API is in this loop, which is the basis of browser scheduling tasks.

definition

In order to coordinate events, user interaction, scripting, rendering, network tasks, etc., the browser must use the event loop described in this section.

technological process

  1. Take one from the task queueMacro taskAnd implement.
  2. Check the micro task queue, execute and emptyMicro taskQueue. If a new micro task is added to the execution of the micro task, it will be executed together in this step.
  3. Enter the update rendering phase to determine whether rendering is needed. Here is onerendering opportunityIn other words, each round of event loop does not necessarily correspond to a browser rendering. It depends on the screen refresh rate, page performance, and whether the page is running in the background. Generally speaking, the rendering interval is fixed. (so multiple tasks are likely to be executed between one rendering.)

    • The browser will keep the frame rate stable as much as possible. For example, if the page performance cannot maintain 60fps (rendering every 16.66ms), the browser will choose the update rate of 30fps instead of occasionally losing frames.
    • If the browser context is not visible, the page will be reduced to about 4 FPS or even lower.
    • If you meet the following conditions, you will also skip rendering:

      1. The browser judges that updating the rendering will not bring about visual changes.
      2. map of animation frame callbacksEmpty, that is, the frame animation callback is empty, which can be accessed through therequestAnimationFrameFrame request animation.
  4. If the above judgment decides this roundNo rendering is required, soThe next few steps will not continue

    This step enables the user agent to prevent the steps below from running for other reasons, for example, to ensure certain tasks are executed immediately after each other, with only microtask checkpoints interleaved (and without, e.g., animation frame callbacks interleaved). Concretely, a user agent might wish to coalesce timer callbacks together, with no intermediate rendering updates.
    Sometimes the browser wants the two timer tasks to be merged, and they are only interspersedmicroTaskInstead of interspersed with screen rendering related processes (e.grequestAnimationFrameWe will write an example below.

  5. For the document to be rendered, if the size of the window changes, execute theresizemethod.
  6. For the document to be rendered, if the page scrolls, executescrollmethod.
  7. For documents that need to be rendered, perform a frame animation callback, that isrequestAnimationFrameCall back. (to be explained later)
  8. For documents that need to be rendered, perform a callback from intersectionobserver.
  9. For documents that need to be rendered,Re renderDraw the user interface.
  10. judgeTask queueandmicroTaskWhether the queue is empty, and if so, proceedIdleThe algorithm of idle cycle to determine whether to executerequestIdleCallbackThe callback function of. (to be explained later)

aboutresizeandscrollIn other words, it is not until this step that scrolling and zooming are performed. Isn’t that a lot of delay? Of course, the browser will help you scroll the view immediately. According to the cssom specification, the browser will save onepending scroll event targets, wait until thescrollIn this step, you can dispatch an event to the corresponding target, and drive it to perform the callback function of monitoring.resizeIt’s the same thing.

In this process, we can take a closer look at the relationship among macro task, micro task and rendering.

Multitask queue

taskThe queue is not the only one we would have imagined, according to the specification:

An event loop has one or more task queues. For example, a user agent could have one task queue for mouse and key events (to which the user interaction task source is associated), and another to which all other task sources are associated. Then, using the freedom granted in the initial step of the event loop processing model, it could give keyboard and mouse events preference over other tasks three-quarters of the time, keeping the interface responsive but not starving other task queues. Note that in this setup, the processing model still enforces that the user agent would never process events from any one task source out of order.

There may beOne or moreTask queues for processing:

  1. Mouse and keyboard events
  2. Other tasks

The browser may assign three-quarters of the priority to mouse and keyboard events on the premise of maintaining the task order, so as to ensure that the user’s input gets the highest priority response, and the remaining priority is given to othersTaskAnd promise not to starve them to death.

This specification also leads to Vue 2.0.0-rc. 7nextTickThe micro task is adoptedMutationObserverChange to macro taskpostMessageAnd it led to an issue.

At present, due to some “unknown” reasons, the jsfiddle case can not be opened. A brief description is the adoption oftaskRealizednextTickWhen the user continues to scrollnextTickThe task was delayed for a long time to perform, resulting in the animation not following the scrolling.

Forced by helpless, you da still changed backmicroTaskTo achievenextTickFor now, of coursepromise.thenMicro tasks have been relatively stable, and chrome has been implementedqueueMicroTaskThis is the official API. In the near future, if we want to call the micro task queue, we can also save instantiationPromiseIt’s spending.

From this issue example, we can see that it is better to have a little in-depth understanding of the specification, so as not to be confused when encountering such a complicated bug.

Let’s talk about it in detail in the following chaptersrequestIdleCallbackandrequestAnimationFrame

requestAnimationFrame

In the followingrequestAnimationFrameReferred to asrAF

In the process of interpreting norms, we find thatrequestAnimationFrameThe callback of has two characteristics:

  1. Call before rendering.
  2. Most likely not called after a macro task.

Let’s analyze why we call it before rendering again? becauserAFIt is the API recommended by the official to be used for some smooth animation. It is inevitable to change DOM when making animation. If you change DOM after rendering, it will only wait until the next rendering opportunity to draw it out. It is obviously unreasonable.

rAFBefore the browser decides to render, it gives you a last chance to change the DOM properties, and then it can be quickly rendered in the following rendering, so this is the best choice for smooth animation. I’ll use one nextsetTimeoutFor example.

Flash animation

Suppose we want to flash on the screen quickly nowredblueTwo colors to ensure users can observe if we usesetTimeoutWrite and take our long-term misconception that “macro tasks must be accompanied by browser rendering,” and you will get an unexpected result.

setTimeout(() => {
  document.body.style.background = "red"
  setTimeout(() => {
    document.body.style.background = "blue"
  })
})

In depth analysis of event loop and browser rendering, frame animation, idle callback (animation demonstration)

We can see that this result is very uncontrollable, if these twoTaskIf you encounter a rendering opportunity recognized by the browser, it will redraw, otherwise it will not. Because the interval between these two macro tasks is too short, there is a high probability that they will not.

If you set the delay to17msThen the probability of redrawing will be much higher, after all, this is the general situation60fpsAn indicator of. However, there will be a lot of non drawing situations, so it is not stable.

If you rely on this API to do animation, it is likely to cause “frame drop”.

Next, let’s change torAFtry? We use a recursive function to simulate 10 color changes in animation.

let i = 10
let req = () => {
  i--
  requestAnimationFrame(() => {
    document.body.style.background = "red"
    requestAnimationFrame(() => {
      document.body.style.background = "blue"
      if (i > 0) {
        req()
      }
    })
  })
}

req()

Because the color changes so fast,gifThe recording software can’t cut out such a high frame rate color transformation, so you can put it into the browser and try it yourself. I’ll directly draw the conclusion. The browser will draw these 10 groups of color changes, that is, 20 color changes. You can see the performance of the performance panel record:

In depth analysis of event loop and browser rendering, frame animation, idle callback (animation demonstration)

Timer merge

In the first section of the specification, point 4 mentioned that timer macro tasks may skip rendering directly.

According to some conventional understanding, macro tasks should be interspersed with rendering, and timer task is a typical macro task. Take a look at the following code:

setTimeout(() => {
  console.log("sto")
  requestAnimationFrame(() => console.log("rAF"))
})
setTimeout(() => {
  console.log("sto")
  requestAnimationFrame(() => console.log("rAF"))
})

queueMicrotask(() => console.log("mic"))
queueMicrotask(() => console.log("mic"))

Intuitively, should the order be:

mic
mic
sto
rAF
sto
rAF

What about it? In other words, each macro task is followed by a rendering.

In fact, the browser will merge the two timer tasks:

mic
mic
sto
sto
rAF
rAF

requestIdleCallback

Interpretation of the draft

In the followingrequestIdleCallbackReferred to asrIC

We all knowrequestIdleCallbackIt is an idle scheduling algorithm provided by the browser. For its introduction, you can see the MDN document. The intention is to let us put some tasks with large amount of calculation but not so urgent to execute in idle time. Don’t affect high priority tasks in the browser, such as animation, user input, etc.

React’s time slice rendering wants to use this API, but now browsers are not supporting it, they suck it up.postMessageA set of.

The rendering takes place in an orderly manner

First of all, take a look at a diagram, which describes the intention of this API very precisely

In depth analysis of event loop and browser rendering, frame animation, idle callback (animation demonstration)

Of course, this orderlyBrowser > User > browser > UserScheduling is based on the premise that we need to split the task into smaller pieces. It can’t be said that the browser has given you the free time, but it takes you a long time to execute it10sIt will definitely block the browser. This requires us to read itrICWhat you havedeadlineIn the time, to dynamically arrange our small tasks. The browser trusts you, and you can’t live up to it.

Render long idle

In depth analysis of event loop and browser rendering, frame animation, idle callback (animation demonstration)
In another case, the browser may be idle for a few frames, and there is no operation that affects the view, so it does not need to draw the page
Why is there still such a situation50msOfdeadlineWhat about it? It’s because browsers respond to some unexpected user interaction in advance, such as user input text. If the given time is too long and your task is stuck in the main thread, the user interaction will not be responded to. 50ms can ensure that users can get response without perceived delay.

The background task collaborative scheduling API in the MDN document is introduced clearly. Let’s do a small experiment according to the concepts in the API

There is a red box in the middle of the screen, which directly copies the animation code of the sample part of the request animation frame in the MDN document.

It is also mentioned in the draft:

  1. When the browser determines that the page is not visible to the user, the callback execution frequency may be reduced to 10 seconds, or even lower. This is also mentioned in the interpretation of EventLoop.
  2. If the browser is busy, there is no guarantee that it will provide free time for executionrICThe callback of, and may be delayed for a long time. So if you need to make sure your task is done in a certain time, you can giverICPass in the second parametertimeout
    This forces browsers to execute after that time, no matter how busy they arerICThe callback function of. So use it with caution, because it interrupts the browser’s higher priority work.
  3. The maximum duration is 50 ms, which is based on research that shows that the response to user input within 100 ms is generally considered instantaneous. Setting the idle deadline to 50ms means that even if user input occurs immediately after the idle task starts, the browser still has the remaining 50ms in which to respond to user input without user perceived lag.
  4. Every calltimeRemaining()When the function judges whether there is any time left, if the browser judges that there is a higher priority task at this time, it will dynamically set this value to 0, otherwise, it will be set in advancedeadline - nowTo calculate.
  5. thistimeRemaining()The calculation is very dynamic and will be determined by many factors, so don’t expect this time to be stable.

Animation example

roll

If my mouse doesn’t do any action and interaction, I just go through the consolerICTo print the remaining time of this idle task, it is generally stable at49.xxMS, because the browser has no higher priority tasks to deal with.

In depth analysis of event loop and browser rendering, frame animation, idle callback (animation demonstration)

And if I keep scrolling the browser and constantly trigger the browser to redraw, this time will become very unstable.

In depth analysis of event loop and browser rendering, frame animation, idle callback (animation demonstration)

In this case, you can feel more physically what is called “busy” and what is “idle”.

animation

The example of this animation is very simple, which is to userAFIn the callback before rendering each frame, move the box position to the right by 10px.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      #SomeElementYouWantToAnimate {
        height: 200px;
        width: 200px;
        background: red;
      }
    </style>
  </head>
  <body>
    <div id="SomeElementYouWantToAnimate"></div>
    <script>
      var start = null
      var element = document.getElementById("SomeElementYouWantToAnimate")
      element.style.position = "absolute"

      function step(timestamp) {
        if (!start) start = timestamp
        var progress = timestamp - start
        element.style.left = Math.min(progress / 10, 200) + "px"
        if (progress < 2000) {
          window.requestAnimationFrame(step)
        }
      }
      //Animation
      window.requestAnimationFrame(step)

      //Idle scheduling
      window.requestIdleCallback(() => {
        alert("rIC")
      })
    </script>
  </body>
</html>

Notice I added one at the endrequestIdleCallbackIn the callbackalert('rIC')Let’s take a look at the demonstration:

In depth analysis of event loop and browser rendering, frame animation, idle callback (animation demonstration)

alertIt was implemented at the very beginning. Why is this? Think about the concept of “idle”. We just put theleftThis simple rendering does not occupy the idle time. Therefore, the browser may find a chance to call it at the very beginningrICThe callback function of.

Let’s make a simple changestepFunction, add a heavy task in it, 1000 cycles of printing.

function step(timestamp) {
  if (!start) start = timestamp
  var progress = timestamp - start
  element.style.left = Math.min(progress / 10, 200) + "px"
  let i = 1000
  while (i > 0) {
    console.log("i", i)
    i--
  }
  if (progress < 2000) {
    window.requestAnimationFrame(step)
  }
}

Let’s take a look at its performance

In depth analysis of event loop and browser rendering, frame animation, idle callback (animation demonstration)

As we expected, because every frame of the browser is “too busy”, it really ignores usrICFunction.

IfrICFunction plus onetimeoutWhat about:

//Idle scheduling
window.requestIdleCallback(
  () => {
    alert("rID")
  },
  { timeout: 500 },
)

In depth analysis of event loop and browser rendering, frame animation, idle callback (animation demonstration)

The browser will be in500msNo matter how busy you are, enforce itrICThis mechanism can prevent us from starving to death.

summary

Through the learning process of this paper, I have also broken many inherent misconceptions about event loop, RAF and ric functions. Through this paper, we can sort out the following key points.

  1. event loopnot alwaysEach round is accompanied by emphasis rendering, but if there are micro tasks, they will be accompaniedMicro task execution
  2. There are many factors that determine whether the browser view is rendered or not. Browsers are very smart.
  3. requestAnimationFrameRe rendering screenbeforeExecution, very suitable for animation.
  4. requestIdleCallbackIn rendering screenafterExecute, and whether there is any time to execute depends on the browser schedule. If you must execute it at a certain time, please usetimeoutParameter.
  5. resizeandscrollIn fact, the event has its own throttling. It only dispatches events to the event loop in the rendering phaseEventTargetGo ahead.

In addition, this article is also the interpretation of the specification. Some terms in the specification are more obscure and difficult to understand, so I have combined some of my own understanding to write this article. If there are any mistakes, please point out.

reference material

HTML specification document

W3C standard

Vue source code of nexttick: mutationobserver is just a floating cloud, microtask is the core! (highly recommended)

❤️ Thank you all

1. If this article is helpful to you, please give me a compliment. Your “like” is the driving force of my creation.

2., pay attention to the official account “the front end from the advanced to the hospital” can add my friend, I pull you into the “front end advanced communication group”, everybody exchanges and advances together.

In depth analysis of event loop and browser rendering, frame animation, idle callback (animation demonstration)