This article will talk about the compilation pipeline of JS engine, the rendering process of rendering engine, and then introduce why the Event loop is needed.

I hope to answer your questions as follows:

  • What is the compilation pipeline of JS engine
  • What does the rendering process do
  • Why event Loop
  • What are the differences between different JS host environments
  • What problems do Micro Task and Check solve
  • RequestAnimationFrame is a macro task or a micro task
  • When was requestIdleCallback implemented

JS engine

composition

The JS engine consists of the Parser, interpreter, GC, and a JIT compiler.

  • Parser: Converts javascript source code to AST
  • Interperter: interpreter responsible for converting AST to bytecode and interpreting execution
  • JIT Compiler: Compiles hot-spot functions at execution time, converting bytecode to machine code, which can then be executed directly
  • Garbage Collector (GC) : cleans objects that are no longer used in the heap memory

Compile pipeline

The usual JS engine pipeline is to parse source code into an AST, which is then converted into bytecode and interpreted to execute bytecode. The runtime collects the frequency of function execution and, for hot code that reaches a certain threshold, converts the bytecode to machine code (JIT) and executes it directly. This is where THE JS code works.

Rendering engine

During rendering, HTML and CSS will be respectively parsed into DOM and CSSOM by Parser, and then merged together, and the layout style will be calculated into absolute coordinates to generate a rendering tree, and then the content of the rendering tree can be copied to video memory to complete rendering by the graphics card.

Each render flow is called a frame, and the browser has a frame rate (say 60 frames per second) to refresh.

How to combine JS engine and rendering engine

Both the JS engine and the rendering engine are silly (pure). The JS engine will only execute THE JS code, and the rendering engine will only layout and render. But to make a complete web application, you need both. How do you combine the two?

There are two ideas:

multithreading

Divided into multiple threads, the main thread is used to manipulate the UI and render, and other threads are used to perform some tasks (you can’t modify the UI by multiple threads at the same time, the order can’t be controlled).

Android UI Architecture

This is the android architecture, you do UI updates, you do event binding, you put all the other logic in other threads, and then you put a message in the message queue, and the main thread loops over and over again to execute the message.

Electron UI framework

Those of you who have developed electron will know that electron is divided into the main process and the renderer process. Operations related to Windows can only be performed in the main process, and the renderer process sends messages to the main process.

From the above two cases, we can conclude that the design of all UI systems, if using multi-threaded (process) architecture, basically the UI can only operate in one thread (process), other threads (process) to send messages to update, if multiple threads, there will be a message queue and looper. The producers of message queues are the child threads (processes) and the consumers are the main thread (processes).

And it’s not just the UI architecture that makes a lot of use of message queues on the back end,

Message queues at the back end

Backend because different service load capacity is different, so the middle will add a message queue to asynchronous message processing, and front-end client UI framework, the backend message queue middleware can have multiple customers, multiple queues, and only one message queue queue of the UI system, a consumer (the main thread, main process).

A common architecture where one thread does UI operations and the other thread does logical calculations requires a message queue for asynchronous message processing. The web worker is an implementation of this architecture later in the web, but it didn’t start out that way.

Single thread

Because javascript was originally designed only for form processing, there is no particularly large amount of computation. Instead of using multi-threaded architecture, DOM manipulation and logical computation are carried out in a single thread, rendering and JS execution blocking each other. (Later, Web worker was added, but not mainstream)

We know that the JS engine only knows how to execute JS and the render engine only knows how to render. They don’t know each other. How can they cooperate?

The answer is the Event loop.

The host environment

The JS engine does not provide event loops (many students may think that event loops are provided by the JS engine, but they are not). It is a mechanism designed by the host environment for aggregate rendering and JS execution, as well as for handling high-priority tasks during JS execution.

Host environments include browsers, Nodes, cross-end engines, etc. Different hosting environments have some differences:

The injected global API is different
  • Node will inject some global REQUIRE APIS and provide built-in modules such as FS and OS
  • Browsers inject W3C standard apis
  • Cross-end engines inject device apis, as well as a set of APIS for manipulating UI (which may or may not be w3c apis)
The implementation of event Loop is different

As mentioned above, event loop is provided by the host environment. Different host environments have different tasks to be scheduled, so there will be different designs:

  • The browser is mainly for scheduling rendering and JS execution, as well as worker
  • Node is mainly used to schedule various IO
  • Cross-end engines also schedule rendering and JS execution

Here we only care about the event loop in the browser.

Event loop for the browser

check

The execution of a JS task in the browser is an event loop. At the end of each loop, it will check whether rendering is needed and whether worker messages need to be processed. In this way, it will check at the end of each loop to integrate rendering, JS execution, worker, etc. Let them all be executed in one thread (rendering is actually in another thread, but blocks with the JS thread).

In this way, the scheduling problems of rendering, JS execution and worker are solved.

But is there a problem with that?

We will continue to put new tasks in the task queue, so that if there is a better task will not be executed until all the tasks have been executed. What if it’s an “emergency”?

So we need to add an “emergency” fast channel to the Event loop, which is micro Tasks.

micro tasks

Tasks are still executed one at a time. After execution, check whether to render or not and process worker messages. However, queue-jumping mechanism is also added for “urgent” with high priority, and all micro tasks will be processed after completion of tasks.

This way, the Event Loop seems to be perfect, checking for render every time, and handling JS “emergencies” faster.

requestAnimationFrame

RequestAnimationFrame = requestAnimationFrame = requestAnimationFrame = requestAnimationFrame = requestAnimationFrame = requestAnimationFrame

If someone asks requestAnimationFrame whether it is a macro or a micro task, you can tell them that requestAnimationFrame is a callback that is executed before rendering is found at the end of the loop, not a macro or micro task.

Event loop problem

As discussed above, although worker is added later, JS calculation and rendering are still the mainstream way of blocking each other, which leads to a problem:

The calculation and rendering of each frame has a fixed frequency. If the JS execution time is too long, exceeding the refresh time of one frame, it will lead to rendering delay and even frame drop (because the data of the last frame is overwritten into new data before rendering to the interface), which will give the user the feeling of “interface is stuck”.

What can cause frame refresh delay or even frame overwrite? Each loop may block the check at each stage prior to rendering the check, i.e. Task, MicroTask, requestAnimationFrame, requestIdleCallback. In this way, it will be too late to render when it is found to be needed during check.

So the main thread JS code should not do too much calculation (unlike Android will naturally start a thread to do), to do split, which is why the UI framework has to do calculation fiber, is to deal with the interaction, do not let the calculation block rendering, recursive change loop, through the linked list to do calculation pause recovery.

In addition to paying attention to the JS code itself, if the browser could provide an API to execute at every frame interval, it would not block, hence the requestIdeCallback.

requestIdleCallback

RequestIdleCallback executes this at the end of each check when there is still time to refresh the next frame. If I don’t have enough time, I’ll talk about it in the next frame.

If there is no time for each frame, it will not work either, so the timeout parameter is provided to specify the maximum waiting time. If there is no time to execute this logic, it will be executed even if the frame is delayed.

This API is very much needed for the front-end framework, which wants calculations to not block rendering, i.e. do calculations at idle time of each frame, but this API is recently added and has compatibility issues. React implements a fiber mechanism similar to Idle Callback to determine how long it is until the next frame refreshes before executing logic.

conclusion

In short, the browser has A JS engine to execute THE JS code, uses the injected browser API to complete the function, and has a rendering engine to render the page. Both are relatively pure and require a scheduling method, which is the Event loop.

The Event Loop implements the Task and emergency handling mechanism microTask, and each time the loop ends it checks whether to render or not. The render is preceded by the requestAnimationFrames life cycle.

Frame refresh cannot be delayed or it will get stuck or even drop frames, so we need to do not do too much calculation in JS code, so we have the API of requestIdleCallback, and hope to execute it when there is still time after each check. If there is no time, it will not be executed (the deadline time is also used as a parameter for the JS code to determine). In order to avoid running out of time, it also provides the timeout parameter to force execution.

Preventing the rendering from taking too long is a constant concern of the UI framework, how to not block the rendering and allow the logic to be broken down into chunks that can be executed within the frame interval. Browsers provide an API for IdelCallback, and many UI frameworks also implement the splitting of computationby recursively changing the loop and then recording the state. The purpose is only one: the logical execution in the loop should not block check, that is, the rendering engine should not block frame refresh. So neither JS code macros nor microtasks, requestAnimationCallback, nor requestIdleCallback should take too long to calculate. This problem is the constant pain of front-end development.