preface

There are a lot of articles about Event Loop, but many of them just talk about “macro tasks” and “micro tasks”. Let me start with a few questions:

  1. Will every turn of the Event Loop be accompanied by a render?

  2. At what stage does the requestAnimationFrame execute, before or after rendering? Before or after microTask?

  3. At what stage is requestIdleCallback executed? How to do that? Before or after rendering? Before or after microTask?

  4. When are events such as resize and scroll distributed?

These questions are not meant to be hard on you. If you don’t know this, you may not be able to choose a requestAnimationFrame properly when you encounter an animation requirement. You may have come up with requestIdleCallback while doing some requirements, but you don’t know when it will run. Just use it with fear and hope you don’t get a bug on the line.

This is also one of the motivations of this paper to dig deep from the bottom of the specification interpretation. Where appropriate, this article will exclude concepts from the specification that are more esoteric or less relevant to the main process. A more detailed version of the specification can also be read directly, but it is more time-consuming and laborious.

Event loop

We’ll start with the browser’s event loop according to the OFFICIAL HTML specification [1], because it’s where the rest of the API goes, and it’s the basis of the browser’s scheduling task.

define

To coordinate events, user interactions, scripting, rendering, network tasks, etc., browsers must use the event loop described in this section.

process

  1. Retrieves a macro task from the task queue and executes it.

  2. Check the microtask queue, execute and clear the microtask queue. If a new microtask is added in the execution of the microtask, it will also be executed in this step.

  3. Enter the update rendering stage to determine whether to render or not, there is a concept of rendering opportunity, which means that not every turn of event loop will correspond to a viewer rendering. It needs to be determined by the screen refresh rate, page performance and whether the page is running in the background. Usually this render interval is fixed. (So it is likely that multiple tasks will execute between renders)

  • The browser will try to keep the frame rate as stable as possible, for example if the page performance cannot be maintained at 60fps (render every 16.66ms), the browser will choose 30fps update rate instead of losing frames occasionally.

  • If the browser context is not visible, the page drops to around 4fps or less.

  • Rendering is also skipped if the following conditions are met:

  1. The browser determines that updating the render will not result in a visual change.

  2. The map of AnimationFrame callbacks is null, i.e. the frame animation callback is null, and the frame animation can be requested via requestAnimationFrame.

  • If the above judgment determines that this round does not need to be rendered, then the following 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 browsers expect the two “timer tasks” to be merged, intersperse with microTask execution and not screen rendering related processes (such as requestAnimationFrame, an example below).

  • For documents that need to be rendered, if the window size changes, the listening resize method is executed.

  • For a document that needs to be rendered, execute the Scroll method if the page is scrolled.

  • For the document that needs to be rendered, the frame animation callback is executed, which is the requestAnimationFrame callback. (More on that later)

  • For documents that need to be rendered, the InterpObserver callback is performed.

  • For documents that need to be rendered, rerender the user interface.

  • Check whether the Task queue and microTask queue are both empty. If so, perform the Idle Idle period algorithm to determine whether to execute the callback function of requestIdleCallback. (More on that later)

  • For resize and Scroll, you don’t have to scroll and scale at this point, wouldn’t that be a lot of delay? According to the CSSOM specification [2], the browser saves a pending Scroll event targets and waits for the Scroll step in the event loop to send an event to the target to execute the listening callback function. The same goes for resize.

    You can take a closer look at the relationship between “macro tasks”, “micro tasks”, and “render” in this flow.

    Multitask queue

    The task queue is not a single queue as we imagine, 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 be one or more task queues in the event loop to process:

    1. Mouse and keyboard events

    2. Other tasks

    The browser may assign three-quarters of the priority to mouse and keyboard events while keeping the order of the tasks, ensuring that the user’s input gets the highest priority response, and giving the rest of the priority to other tasks without “starving” them.

    This specification also resulted in Vue 2.0.0-RC.7, in which nextTick changed from a microtask MutationObserver to a macro task postMessage, resulting in an Issue[3].

    The Jsfiddle case is now closed for “unknown” reasons. A brief description of nextTick is the implementation of task. When the user keeps scrolling, the nextTick task is delayed for a long time and the animation can’t keep up with the scrolling.

    The promise. Then microtasks are now stable, and Chrome has implemented queueMicroTask as an official API. In the near future, we can also save on the overhead of instantiating promises when we want to invoke microtask queues.

    As we can see from the example of this Issue, it is good to know a little more about the specification, so as not to be confused when encountering such a complex Bug.

    We’ll talk more about requestIdleCallback and requestAnimationFrame in the following sections.

    requestAnimationFrame

    RequestAnimationFrame is referred to as rAF for short in the following

    While reading the specification, we discovered that the requestAnimationFrame callback has two characteristics:

    1. Called before rerendering.

    2. Most likely not called after the macro task.

    Let’s analyze, why is it called before rerendering? Since rAF is the official recommended API for smooth animation, animation will inevitably change the DOM, and if you change the DOM after rendering, it will have to wait until the next rendering opportunity to draw, which is obviously not reasonable.

    RAF gives you one last chance to change the DOM properties before the browser decides to render, and then quickly renders them for you in subsequent drawings, so it’s a great choice for smooth animation. Let me use a setTimeout example for comparison.

    Flash animation

    Let’s say we want to quickly flash red and blue on the screen so that the user can see it. If we write with setTimeout, with the long-standing misconception that macro tasks must be drawn between browser tasks, you’ll get an unexpected result.

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

    As you can see, the result is very uncontrollable, and if the two tasks encounter what the browser considers a rendering opportunity, it will redraw, otherwise it won’t. Because the interval between the two macro tasks is so short, there is a high probability that it will not.

    If you set the delay to 17ms then the redrawing probability is much higher, after all, this is a normal indicator of 60fps. But there are a lot of situations where you don’t draw, so it’s not stable.

    If you rely on this API to animate, you are likely to “drop frames”.

    Let’s try rAF instead. We use a recursive function to simulate the animation of 10 color changes.

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

    Here due to the color change too fast, GIF recording software can not cut out such a high frame rate of color transformation, so you can put in the browser to try their own execution, I directly throw the conclusion, the browser will be very regular to draw the 10 groups of 20 color changes, The performance panel records the performance:

    Timer merge

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

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

    Intuitively, the order should be:

    micmicstorAFstorAF
    Copy the code

    ? That is, every macro task is followed by a render.

    No, the browser will merge the two timer tasks:

    micmicstostorAFrAF
    Copy the code

    requestIdleCallback

    The draft interpretation

    RequestIdleCallback is abbreviated to rIC in the following.

    We all know that requestIdleCallback is an idle scheduling algorithm provided to us by the browser. A brief introduction of requestIdleCallback can be found in the MDN documentation [4], which is intended to allow us to perform some of the more computation-intensive but less urgent tasks in the idle time. Do not interfere with higher-priority tasks in the browser, such as animation, user input, and so on.

    React’s timesarding rendering is intended to use this API, but it is currently not supported by browsers. They implemented it themselves using postMessage.

    Render in order

    Let’s start with a diagram that accurately describes the intent of the API:

    Of course, this orderly browser-user-browser-user scheduling is based on the premise that we need to divide tasks into smaller pieces. We can’t say that the browser gives you free time. If you try to execute a task that takes 10 seconds, the browser will definitely block. This requires us to read the time in the deadline provided by rIC to you and dynamically arrange our small tasks. The browser trusts you, and you can’t let it down.

    Render long idle

    Alternatively, it is possible for the browser to be idle for a few frames and not do anything to affect the view, so it does not need to draw the page: why is that still the case50msdeadline? This is because browsers are prepared for unexpected user interactions, such as user input. If you give it too much time, your task gets stuck on the main thread, and the user’s interaction goes unanswered. 50ms ensures that the user gets a response with no perceived delay.

    The behind-the-scenes task cooperative scheduling API [5] in the MDN document is clearly introduced, so let’s do a small experiment according to the concepts in it:

    There is a red square in the middle of the screen that directly copies the animation code from the sample section of requestAnimationFrame[6] in the MDN document.

    The draft also states:

    1. When the browser determines that the page is not visible to the user, the frequency of this callback can be reduced to once every 10 seconds, or even less. This is also mentioned in interpreting EventLoop.

    2. If the browser is busy, there is no guarantee that it will provide free time to execute rIC callbacks, and it may be delayed for a long time. So if you need to ensure that your task will be executed within a certain amount of time, you can pass rIC a second timeout. This forces the browser, no matter how busy it is, to execute rIC’s callback after this time. So use it with caution, as it interrupts the browser’s own higher-priority work.

    3. The maximum duration of 50 milliseconds is based on research showing that people generally think of responses to user input within 100 milliseconds as instantaneous. Setting the idle deadline to 50ms means that even if user input occurs immediately after the idle task starts, the browser still has a remaining 50ms in which to respond to user input without perceptible lag to the user.

    4. Every time the timeRemaining() function is called to determine if there is any time left, the browser dynamically sets this value to 0 if it thinks there is a higher priority task, otherwise it uses the preset deadline-now function to calculate the remaining time.

    5. The calculation of timeRemaining() is very dynamic and depends on many factors, so don’t expect this time to be stable.

    Animation example

    rolling

    If I print the remaining time of the idle task through rIC on the console without any mouse action or interaction, it is generally stable at 49.xx ms, because the browser has no higher priority tasks to deal with at this time.

    If I keep scrolling through the browser, constantly triggering the browser to redraw, this time becomes very unstable.

    By using this example, you can get a better sense of what is “busy” and what is “idle”.

    animation

    The example of this animation is very simple, using rAF to move the position of the block 10px to the right in the callback before each frame rendering.

    <! 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: 200 px; 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) / / Free scheduling window. RequestIdleCallback (() = > {alert (" rIC ")}) < / script > < / body > < / HTML >Copy the code

    Note that at the end I added a function called requestIdleCallback that calls alert(‘rIC’).

    Alert is carried out in the beginning, why would be so, think about the concept of “leisure”, we each frame is just move the left value, made it a simple rendering, not fill your free time, so maybe in the beginning, the browser will find opportunities to invoke the rIC callback function.

    Let’s simply change the step function and add a heavy task to 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) }}Copy the code

    Here’s how it behaves:

    As expected, the browser was “too busy” with every frame, so it literally ignored our rIC function.

    How about adding a timeout to rIC:

    / / free scheduling window. RequestIdleCallback (() = > {alert (" rID ")}, {timeout: 500},)Copy the code

    Browsers enforce rIC functions at around 500ms, no matter how busy they are. This mechanism prevents our idle tasks from starving to death.

    conclusion

    Through the learning process of this paper, I also broke a lot of inherent wrong cognition of Event Loop, rAF and rIC functions. Through this paper, we can sort out the following key points.

    1. The event loop is not necessarily accompanied by highlighting every round, but it is certainly accompanied by microtask execution.

    2. There are many factors that determine whether a browser view is rendered or not, and browsers are very smart.

    3. RequestAnimationFrame is executed before the screen is rerendered and is perfect for animation.

    4. The requestIdleCallback is executed after rendering the screen, and whether there is time to execute depends on the browser’s schedule. If you must have it executed within a certain time, use the timeout parameter.

    5. The resize and Scroll events are their own throttles that execute events only during the render phase of the Event Loop.

    In addition, this article is also an interpretation of the specification. Some terms in the specification are difficult to understand, so I have combined some of my own understanding to write this article. If there is any mistake, you are welcome to point out.

    The resources

    HTML specification document [7]

    The W3C standards [8]

    NextTick: MutationObserver is just floating around, microTask is the core! [9] (Highly recommend this article)

    The resources

    [1]

    HTML official specification:

    Html.spec.whatwg.org/multipage/w…

    [2]

    CSSOM spec:

    Drafts.csswg.org/cssom-view/…

    [3]

    Issue:

    Github.com/vuejs/vue/i…

    [4]

    MDN documents:

    Developer.mozilla.org/zh-CN/docs/…

    [5]

    Behind-the-scenes Collaborative Task scheduling API:

    Developer.mozilla.org/zh-CN/docs/…

    [6]

    requestAnimationFrame:

    Developer.mozilla.org/zh-CN/docs/…

    [7]

    HTML specification document:

    Html.spec.whatwg.org/multipage/w…

    [8]

    The W3C standards:

    The w3c. Making. IO/requestidle…

    [9]

    NextTick: MutationObserver is just floating around, microTask is the core! :

    Segmentfault.com/a/119000000…

Source: blog.csdn.net/LuckyWinty/…