banner
 Sayyiku

Sayyiku

Chaos is a ladder
telegram
twitter

The JS execution mechanism parsed by the browser

Processes and Threads#

Processes and threads can be metaphorically compared to factories and workers.

  • A process is a factory, which has its own independent resources (a separate block of memory allocated by the system).
  • Factories are independent of each other (processes are independent of each other).
  • A thread is a worker in the factory, and multiple workers collaborate to complete tasks (multiple threads collaborate to complete tasks within a process).
  • A factory can have one or more workers (a process consists of one or more threads).
  • Workers share space (threads within the same process share the program's memory space).

It can be understood that a process is the smallest unit that can own resources and run independently, while a thread is a unit of program execution built on the foundation of a process. A process can have multiple threads. The official terminology is:

  • A process is the smallest unit of CPU resource allocation.
  • A thread is the smallest unit of CPU scheduling.

Different processes can also communicate, but the cost is relatively high. The common terminology of single-threaded and multi-threaded generally refers to single and multiple within a process.

Browser Multi-Process#

Three concepts need to be understood regarding the browser:

  • The browser is multi-process.
  • The reason the browser can run is that the system allocates resources (CPU, memory) to its processes.
  • Each time a tab is opened, it is equivalent to creating an independent browser process.

Introduction to Browser Multi-Process#

Processes included in the browser:

  1. Browser Process: The main process of the browser (responsible for coordination and control), there is only one, with functions including:
  2. Responsible for displaying the browser interface and interacting with the user, such as going forward, going back, etc.
  3. Responsible for managing various pages, creating and destroying other processes.
  4. Drawing the Bitmap obtained from the Renderer process's memory onto the user interface.
  5. Managing network resources, downloads, etc.
  6. Third-Party Plugin Process: Each type of plugin corresponds to a process, created only when that plugin is used.
  7. GPU Process: There can be at most one, used for 3D rendering, etc.
  8. Renderer Process (the browser rendering process, which is multi-threaded internally): The Renderer process refers to the browser kernel, with a default process for each tab page, which do not affect each other. Its main functions are page rendering, script execution, event handling, etc.

Advantages of browser multi-process:

  • Avoids a single page crash affecting the entire browser.
  • Avoids a third-party plugin crash affecting the entire browser.
  • Multi-process fully utilizes multi-core advantages.
  • Facilitates the use of a sandbox model to isolate plugins and other processes, improving browser stability.

Renderer Process#

We need to focus on the Renderer process, which is the browser rendering process. The main threads included in this process are:

  1. GUI Rendering Thread
  2. Responsible for rendering the browser interface, parsing HTML, CSS, constructing the DOM tree and RenderObject tree, layout, and painting, etc.
  3. This thread executes when the interface needs to be repainted or when a reflow is triggered by some operation.
  4. Note that the GUI rendering thread and the JS engine thread are mutually exclusive; when the JS engine is executing, the GUI thread will be suspended, and GUI updates will be saved in a queue to be executed immediately when the JS engine is free.
  5. JS Engine Thread
  6. Also known as the JS kernel (e.g., V8 engine), responsible for parsing JavaScript scripts and executing code.
  7. The JS engine continuously waits for tasks to arrive in the task queue and processes them. There is only one JS thread running JS programs in a tab page (renderer process) at any time.
  8. Similarly, note that the GUI rendering thread and the JS engine thread are mutually exclusive, so if JS execution takes too long, it can cause the rendering of the page to be discontinuous, leading to page rendering load blocking.
  9. Event Trigger Thread
  10. Belongs to the browser rather than the JS engine, used to control the event loop (it can be understood that the JS engine is too busy and needs the browser to open another thread for assistance).
  11. When the JS engine executes code blocks like setTimeout (which can also come from other threads in the browser kernel, such as mouse clicks, AJAX asynchronous requests, etc.), it adds the corresponding task to the event thread.
  12. When the corresponding event meets the trigger conditions, this thread will add the event to the end of the pending processing queue, waiting for the JS engine to process it.
  13. Note that due to the single-threaded nature of JS, all events in the pending processing queue must wait in line for the JS engine to be free to process them.
  14. Timer Trigger Thread
  15. The thread for setInterval and setTimeout.
  16. The browser's timer counter is not counted by the JavaScript engine because the JavaScript engine is single-threaded. If it is in a blocking thread state, it will affect the accuracy of the timing. Therefore, a separate thread is used to time and trigger timers, and once the timing is complete, it is added to the event queue, waiting for the JS engine to be free to execute.
  17. Note that the W3C stipulates in the HTML standard that any time interval below 4ms in setTimeout is counted as 4ms.
  18. Asynchronous HTTP Request Thread
  19. In XMLHttpRequest, a new thread is opened by the browser after the connection.
  20. When a state change is detected, if a callback function is set, the asynchronous thread generates a state change event, places this callback back into the event queue, and is then executed by the JavaScript engine.

Communication Between Browser Process and Renderer Process#

Next, let's analyze the communication method between the Browser process and the Renderer process:

  • When the Browser process receives a user request, it first needs to obtain the page content (for example, by downloading resources over the network), and then it passes this task to the Renderer process through the RendererHost interface.
  • The Renderer interface of the Renderer process receives the message, interprets it, and hands it over to the rendering thread, which then begins rendering.
  • The rendering thread receives the request, loads the webpage, and renders it. This may require the Browser process to obtain resources and the GPU process to assist with rendering.
  • There may be JS threads operating on the DOM (which may cause reflows and repaints).
  • Finally, the Renderer process passes the results back to the Browser process.
  • The Browser process receives the results and renders them.

Multi-Threading in the Renderer Process#

From the analysis above, we know that the Renderer process is multi-threaded, mainly including: GUI rendering thread, JS engine thread, event trigger thread, timer trigger thread, asynchronous HTTP request thread. For these threads, we need to understand some concepts.

GUI Rendering Thread and JS Engine Thread are Mutually Exclusive#

As mentioned above, the GUI rendering thread and the JS engine thread are mutually exclusive. Since JavaScript can manipulate the DOM, if the properties of these elements are modified while rendering the interface (i.e., the JS thread and the UI thread are running simultaneously), the data obtained by the rendering thread before and after may be inconsistent.

Therefore, to prevent rendering from producing unpredictable results, the browser sets the GUI rendering thread and the JS engine thread to be mutually exclusive. When the JS engine is executing, the GUI thread will be suspended, and GUI updates will be saved in a queue to be executed immediately when the JS engine thread is free.

From this mutual exclusion relationship, it can be inferred that if JS execution time is too long, it will block the page. For example, if the JS engine is performing a massive calculation, even if the GUI has updates, they will be saved in the queue, waiting for the JS engine to be free to execute. Then, due to the massive calculation, the JS engine may take a long time to become free, leading to a feeling of extreme lag. Therefore, it is best to avoid long JS execution times, as this can cause the rendering of the page to be discontinuous, leading to a feeling of page rendering load blocking.

Web Worker#

Regarding Web Worker, the introduction from MDN is as follows:

Web Workers provide a simple way for web content to run scripts in background threads. The threads can perform tasks without interfering with the user interface. Additionally, they can use XMLHttpRequest for I/O (although responseXML and channel properties are always empty). Once created, a worker can send messages to the JavaScript code that created it by posting messages to the event handler specified by that code (and vice versa).

The role of Web Workers is to create a multi-threaded environment for JavaScript, allowing the main thread to create Worker threads and assign some tasks to them for execution. While the main thread is running, the Worker thread runs in the background, and the two do not interfere with each other. Once the Worker thread completes its computational tasks, it returns the results to the main thread. The benefit of this is that some computation-intensive or high-latency tasks are handled by the Worker thread, allowing the main thread (which is usually responsible for UI interaction) to run smoothly without being blocked or slowed down.

Once a Worker thread is successfully created, it will run continuously and will not be interrupted by activities on the main thread (such as user clicks or form submissions). This is beneficial for responding to communications from the main thread at any time. However, this also makes Workers resource-intensive, so they should not be overused, and once they are no longer needed, they should be closed.

Browser Rendering Process#

The browser content rendering can be roughly divided into the following steps:

  1. Parse HTML to build the DOM tree.
  2. Parse CSS to construct the render tree (the CSS code is parsed into a tree-like data structure, which is then combined with the DOM to form the render tree).
  3. Layout the render tree (Layout/reflow), responsible for calculating the size and position of each element.
  4. Paint the render tree (paint), rendering the pixel information of the page.
  5. The browser sends the information of each layer to the GPU, which composites the layers and displays them on the screen.

After rendering is complete, the load event is executed, and the flowchart is as follows:

Load Event and DOMContentLoaded Event#

Before comparing the execution order of the load event and the DOMContentLoaded event, let's understand their respective triggering timings:

  • The DOMContentLoaded event is triggered only when the DOM has finished loading, excluding stylesheets and images (for example, if there are async loading scripts, they may not be complete).
  • The onload event is triggered when all DOM, stylesheets, scripts, and images on the page have been fully loaded.

From the above, we can see that the execution order is DOMContentLoaded -> load.

Does CSS Loading Block DOM Tree Rendering?#

Before answering this question, it is important to know a key concept: CSS is downloaded asynchronously by a separate download thread.

We can then conclude: CSS loading does not block DOM tree parsing (the DOM is constructed as usual during asynchronous loading), but it does block render tree rendering (rendering must wait for CSS to finish loading because the render tree requires CSS information).

This is an optimization mechanism of the browser because loading CSS may modify the styles of the subsequent DOM nodes. If CSS loading does not block render tree rendering, then when CSS finishes loading, the render tree may need to be repainted or reflowed again, causing unnecessary overhead. Therefore, it is better to first parse the structure of the DOM tree, complete the work that can be done, and then render the render tree based on the final styles after CSS loading is complete. This approach indeed performs better in terms of performance.

Ordinary Layers and Composite Layers#

In step 5 of the browser rendering process, we mentioned: the browser sends the information of each layer to the GPU, which composites the layers and displays them on the screen. This involves the concept of composite. The layers rendered by the browser generally fall into two categories: ordinary layers and composite layers.

First, the ordinary document flow can be understood as a composite layer (referred to as the default composite layer here; no matter how many elements are added inside, they are all in the same composite layer). Although absolute and fixed layouts can break away from the ordinary document flow, they still belong to the default composite layer.

Then, a new composite layer can be declared through hardware acceleration, which will allocate resources separately and, of course, will also break away from the ordinary document flow. This way, no matter how this composite layer changes, it will not affect the reflow and repaint of the default composite layer.

It can be understood that in the GPU, each composite layer is drawn separately, so they do not affect each other. This is why hardware acceleration works exceptionally well in certain scenarios.

Ways to turn DOM elements into composite layers (hardware acceleration) include:

  • translate3d, translateZ
  • opacity property/transition animation (a composite layer will be created only during the execution of the animation; if the animation has not started or ended, the element will return to its previous state)
  • will-change property, which informs the browser in advance that a change will occur, allowing the browser to start some optimization work (it is best to release it after use).
  • Elements like <video><iframe><canvas><webgl>.

From the above analysis, we can see the difference between absolute and hardware acceleration: although absolute can break away from the ordinary document flow, it cannot break away from the default composite layer. Therefore, even though changes in absolute do not affect the render tree in the ordinary document flow, the browser ultimately draws the entire composite layer. Thus, changes in absolute will still affect the drawing of the entire composite layer. The browser will repaint it, and if there is a lot of content in the composite layer, the changes brought by absolute can consume significant resources.

In contrast, hardware acceleration is directly in another composite layer, so its changes do not affect the default composite layer; it only affects its own composite layer, merely triggering the final composition (output view).

Although hardware acceleration seems wonderful, it should still be used with caution. Avoid using too many composite layers; otherwise, due to excessive resource consumption, the page may become even more sluggish.

When using hardware acceleration, try to use index as much as possible to prevent the browser from creating composite layer rendering for subsequent elements by default. The reason is that in webkit CSS3, if an element has hardware acceleration and a relatively low index level, subsequent elements behind this element will default to composite layer rendering. If not handled properly, it can significantly affect performance.

Event Loop#

Finally, we come to the core part of this article: the JS execution mechanism. Let's review the five threads of the Renderer process: GUI rendering thread, JS engine thread, event trigger thread, timer trigger thread, asynchronous HTTP request thread.

Now, let's understand a concept:

  • JS is divided into synchronous tasks and asynchronous tasks.
  • Synchronous tasks are executed on the main thread (JS engine thread), forming an execution stack.
  • Outside the main thread, the event trigger thread manages a task queue. Whenever an asynchronous task has a running result, an event is placed in the task queue.
  • Once all synchronous tasks in the execution stack are completed, the system will read the task queue and add executable asynchronous tasks to the executable stack to start execution.

This can be explained as follows:

  • The main thread runs the execution stack, and when the code in the stack calls certain APIs (like AJAX requests), events are generated and added to the task queue.
  • Once the code in the execution stack is completed, it reads the code from the task queue, and this process continues.

Macrotask and Microtask#

In JS, there are two types of tasks: macrotask and microtask, which correspond to macro tasks and micro tasks. In ECMAScript, macrotask is referred to as task, and microtask is referred to as jobs.

  • Macrotask: It can be understood that each execution of the stack code is a macro task (including each time an event callback is retrieved from the event queue and placed into the execution stack for execution), maintained by the event trigger thread.
  • Each task will be executed from start to finish without executing others.
  • To ensure that JS internal tasks and DOM tasks are executed in order, the browser will re-render the page after one task is completed and before the next task begins (task->render->task->...).
  • Microtask: It can be understood as tasks that are executed immediately after the current task ends, occurring after the current task but before the next task, and also before rendering, maintained by the JS engine thread.
  • Thus, its response speed is faster than setTimeout (setTimeout is a task) because it does not need to wait for rendering.
  • After a macrotask is completed, all microtasks generated during its execution will be executed (before rendering).

What are the macrotasks and microtasks?

  • Macrotask: Main code block, setTimeout, setInterval.
  • Microtask: Promise, process.nextTick (in the Node environment, process.nextTick has a higher priority than Promise).

Finally, let's summarize the operation mechanism of macrotask and microtask:

  • Execute a macrotask (if there is none in the stack, retrieve one from the event queue).
  • If a microtask is encountered during execution, add it to the microtask queue.
  • After the macrotask is completed, immediately execute all microtasks in the current microtask queue (in order).
  • After the current macrotask is completed, check for rendering, and then the GUI thread takes over rendering.
  • After rendering is complete, the JS thread continues to take over and starts the next macrotask (retrieved from the event queue).

Reference article:

From Browser Multi-Process to JS Single Thread, the Most Comprehensive Review of JS Execution Mechanism

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.