preface

The core of the browser refers to the most core procedures that support the browser to run. It is divided into two parts, one is the rendering engine, the other is the JS engine. Rendering engines are not always the same in different browsers. Currently, there are four common browser cores: Trident (IE), Gecko (Firefox), Blink (Chrome, Opera), and Webkit (Safari). The most familiar of these is probably the Webkit kernel, which is the real king of the browser world.

In this article, we take Webkit as an example to analyze the rendering process of modern browsers in depth.

Page loading process

Before introducing the browser rendering process, let’s briefly describe the loading process of the next page to help you understand the subsequent rendering process.

The main points are as follows:

  • Type the url into your browser
  • The browser obtains the IP address of the domain name from the DNS server
  • Send an HTTP request to the machine at this IP
  • The server receives, processes, and returns HTTP requests
  • The browser receives the content returned by the server

For example, enter www.baidu.com in the browser, and after DNS resolution, the IP address of www.baidu.com is 14.215.177.38 (the corresponding IP address may vary with time and location). The browser then sends an HTTP request to that IP.

The server receives the HTTP request, calculates (pushes different content to different users), and returns the HTTP request with the following content:

It’s just a bunch of HMTL strings, because HTML is the only format that browsers can parse correctly, as required by the W3C standard. Next comes the browser’s rendering process.

Browser rendering process

From the image above, we can see that the browser renders as follows:

Parse HTML, generate DOM tree, parse CSS, generate CSSOM tree

Combine DOM Tree and CSSOM Tree to generate Render Tree

Layout(backflow): According to the generated rendering tree, backflow (Layout), get the geometry information of nodes (position, size)

Painting(repainting): Get absolute pixels of nodes based on the rendered tree and geometry information from backflow

Display: Send the pixel to the GPU, and finally draw it by calling the API of the operating system Native GUI and Display it on the page. (There is more to this step, such as merging multiple compositing layers into one layer on the GPU and displaying them on a page. Css3 hardware acceleration works by creating a new composition layer, which we won’t expand here, but will be covered in a later blog post.)

The rendering process doesn’t look too complicated, so let’s look at what’s going on in each step.

Build the DOM detailed process

The browser follows a set of steps to convert the HTML file into a DOM tree. Macroscopically, it can be divided into several steps:

The browser reads the raw bytes of HTML (byte data) from disk or the network and converts them to strings based on the specified encoding of the file (such as UTF-8).

All the stuff that’s going around the network is zeros and ones. When the browser receives these bytes of data, it converts them into a string, which is the code we wrote.

Convert strings into tokens, such as < HTML >, , etc. The Token will indicate whether the current Token is “start tag”, “end tag”, or “text”.

At this time you must have a question, how to maintain the relationship between nodes?

In fact, that’s what tokens are supposed to do with tokens like “start tag” and “end tag.”

For example, the node between the start tag and end tag of the title Token must be a child node of the head Token.

The figure above shows the relationship between nodes, for example: The “Hello” Token is located between the “title” start tag and the “title” end tag, indicating that the “Hello” Token is a child node of the “title” Token. Similarly, the title Token is the child node of the Head Token.

Generate node objects and build the DOM

In fact, in the process of DOM construction, instead of generating node objects after all tokens are converted, the node objects are generated by consuming tokens while generating tokens. In other words, as soon as each Token is generated, the Token is consumed to create a node object. Note: Tokens with an end tag identifier do not create node objects.

Let’s take an example. Suppose we have some HTML text:

<html>
<head>
    <title>Web page parsing</title>
</head>
<body>
    <div>
        <h1>Web page parsing</h1>
        <p>This is an example Web page.</p>
    </div>
</body>
</html>
Copy the code

The above HTML will parse like this:

Build the CSSOM detail process

The DOM captures the content of the page, but the browser also needs to know how the page is presented, so you need to build CSSOM.

The process of building a CSSOM is very similar to the process of building a DOM. When a browser receives a piece of CSS, the first thing the browser does is recognize the Token, then build the node and generate the CSSOM.

During this process, the browser determines what the style of each node is, and this process can be very resource-intensive. Because styles can be set to a node or inherited. In this process, the browser recurses through the CSSOM tree and determines exactly what style the specific elements are.

Note: CSS matching HTML elements is a fairly complex and performance problem. Therefore, the DOM tree should be small, CSS should try to use ID and class, do not transition layer on layer.

Building a Render tree

Once we have generated the DOM tree and the CSSOM tree, we need to combine the two trees into a render tree.

In this process, it is not as simple as merging the two. The render tree contains only the nodes that need to be displayed and their style information. If a node is display: None, it will not be displayed in the render tree.

Note: The render tree contains only visible nodes

We may have a question: what will browsers do if they encounter JS files during rendering?

During rendering, if

That said, if you want the first screen to render as quickly as possible, you should not load JS files on the first screen, which is why it is recommended to place the script tag at the bottom of the body tag. At the moment, of course, it’s not necessary to put the script tag at the bottom, as you can add defer or async attributes to the script tag (the difference between the two is described below).

The JS file doesn’t just block DOM building, it can cause CSSOM to block DOM building as well.

Originally, DOM and CSSOM were constructed independently of each other, but once JavaScript was introduced, CSSOM also started blocking DOM construction, and only after the COMPLETION of CSSOM construction, DOM construction resumed.

What’s going on here?

This is because JavaScript can not only change the DOM, it can also change styles, which means it can change CSSOM. Because incomplete CSSOM cannot be used, JavaScript must get the full CSSOM when executing JavaScript if it wants to access it and change it. As a result, if the browser has not finished downloading and building CSSOM, and we want to run the script at this point, the browser will delay script execution and DOM building until it has finished downloading and building CSSOM. That is, in this case, the browser downloads and builds CSSOM, then executes JavaScript, and then continues to build the DOM.

Layout and Drawing

When the browser generates the render tree, the layout (also known as backflow) is performed based on the render tree. All the browser has to do at this stage is figure out the exact location and size of each node on the page. This behavior is often referred to as “automatic reordering.”

The output of the layout process is a “box model” that accurately captures the exact position and size of each element within the viewport, and all relative measurements are translated into absolute pixels on the screen.

Immediately after the layout is complete, the browser issues “Paint Setup” and “Paint” events that convert the render tree into pixels on the screen.

backflow

By constructing the render tree, we combine the visible DOM nodes with their corresponding styles, but we also need to calculate their exact position and size in the device viewport. This stage of calculation is called backflow.

To figure out the exact size and location of each object on the site, the browser traverses from the root of the render tree, which can be represented in the following example:

<! DOCTYPEhtml>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Critial Path: Hello world!</title>
  </head>
  <body>
    <div style="width: 50%">
      <div style="width: 50%">Hello world!</div>
    </div>
  </body>
</html>
Copy the code

You can see that the first div sets the display size of the node to 50% of the viewport width, and the second div sets its size to 50% of the parent node. In the backflow phase, we need to convert the viewport to the actual pixel value depending on the width of the viewport. (As shown below)

redraw

Finally, we know which nodes are visible, the style of the visible nodes and the specific geometric information (location, size) by constructing the render tree and the backflow phase, so we can convert each node of the render tree into an actual pixel on the screen. This phase is called the redraw node.

Now that we know how the browser renders, let’s talk about when backflow redraws occur.

When reflux redraw occurs

As we have already seen, the backflow phase mainly computs the location and geometry of nodes, so when the page layout and geometry information changes, backflow is needed.

For example, backflow occurs when:

Depending on the extent and extent of the change, large or small portions of the rendering tree need to be recalculated. Some changes can trigger a rearrangement of the entire page, such as when a scrollbar appears or when a root node is modified.

  • When the page is first rendered (which is inevitable)
  • Browser window size changes (because backflow calculates element position and size based on viewport size)
  • Add or remove visible DOM elements
  • The position of the element changes
  • The size of the element changes (including margins, inner borders, border size, height, width, etc.)
  • Content changes, such as text changes or an image being replaced by another image of a different size.
  • Element font size changes
  • Activate CSS pseudo-classes (such as: :hover)

Some common properties and methods that cause backflow:

ClientWidth, clientHeight, clientTop, clientLeft offsetWidth, offsetHeight, offsetTop, offsetLeft ScrollWidth, scrollHeight, scrollTop, scrollLeft scrollIntoView(), scrollIntoViewIfNeeded() getBoundingClientRect() scrollTo()Copy the code

The following occurs when redrawing occurs without reflow

When a change in the style of an element in a page does not affect its position in the document flow (color, background-color, visibility, etc.), the browser assigns the new style to the element and redraws it, redrawing it without redrawing.

Note: Backflow must trigger redraw, and redraw does not necessarily backflow

Performance impact

Reflux is more expensive than drawing.

Sometimes even if only a single element is backflowed, its parent element and any elements that follow it will also backflow.

Browser optimization mechanism

Modern browsers are optimized for frequent backflow or redraw operations:

The browser maintains a queue that queues all operations that cause backflow and redraw, and if the number of tasks or time intervals in the queue reaches a threshold, the browser emptying the queue and doing a batch, thereby turning multiple backflow and redraw into one.

The browser clears the queue immediately when you access the following properties or methods:

ClientWidth, clientHeight, clientTop, clientLeft offsetWidth, offsetHeight, offsetTop, offsetLeft ScrollWidth, scrollHeight, scrollTop, scrollLeft width, height getComputedStyle()Copy the code

Because there may be operations in the queue that affect the return value of these properties or methods, even if the information you want is not related to the change caused by the operation in the queue, the browser will force the queue to empty to make sure you get the most accurate value.

All of the above attributes and methods need to return the latest layout information, so the browser has to clear the queue and trigger a backflow redraw to return the correct value. Therefore, it is best to avoid using the attributes listed above when modifying styles, as they all refresh the render queue. If you want to use them, it is best to cache the values.

Reduce backflow and redraw

  • Use transform instead of top
  • Use visibility instead of display: None, because the former will only cause redraw and the latter will cause backflow (changing the layout)
  • Do not put node property values in a loop as variables in the loop.
  • Do not use the table layout, it is possible that a small change will cause the entire table to be rearranged
  • Select the speed of the animation implementation, the faster the animation, the more backflows, you can also choose to use requestAnimationFrame
  • CSS selectors match from right to left to avoid too many node hierarchies
  • Set nodes that are frequently redrawn or reflow as layers to prevent their rendering behavior from affecting other nodes. For the video TAB, for example, the browser automatically turns the node into a layer.

Minimize backflow and redraw

Since backflow and redraw can be expensive, it is best to reduce the number of times it occurs. To reduce the number of occurrences, we can merge the DOM and style changes multiple times and then dispose of them all at once. Consider this example

const el = document.getElementById('test');
el.style.padding = '5px';
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
Copy the code

In the example, there are three style attributes that have been modified, each of which affects the element geometry and causes backflow. Of course, most modern browsers are optimized for this, so that only one rearrangement is triggered. However, if the layout information (which triggers backflow above) is accessed by other code in an older browser or while the above code is executing, this will result in three rearrangements.

Therefore, we can merge all the changes and deal with them in sequence. For example, we can do the following:

  • 1. Use cssText
const el = document.getElementById('test');
el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px; ';
Copy the code
  • 2. Use class to wrap CSS styles in a class and change the CSS class
.active{
    border-left: 1px;
    border-right: 2px;
    padding: 5px;
}
Copy the code
const el = document.getElementById('test');
el.className += ' active';
Copy the code

Batch modify DOM

When we need to make a series of DOM changes, we can reduce the number of backflow redraws by following these steps:

  • Takes the element out of the document flow
  • Modify it multiple times
  • Bring the element back into the document.

The first and third steps of the process may cause backflow, but after the first step, any changes to the DOM will not cause backflow because it is no longer in the render tree.

There are three ways to take the DOM out of document flow:

  • Hide elements, apply changes, and redisplay
  • Use document fragments to build a subtree outside the current DOM and copy it back into the document.
  • Copy the original element to a node out of the document, modify the node, and replace the original element.

So let’s do an example

We will execute a batch insert node code:

function appendDataToElement(appendToElement, data) {
    let li;
    for (let i = 0; i < data.length; i++) {
        li = document.createElement('li');
        li.textContent = 'text'; appendToElement.appendChild(li); }}const ul = document.getElementById('list');
appendDataToElement(ul, data);
Copy the code

If we did this directly, we would cause the browser to flow back once, since each loop would insert a new node.

There are three ways to optimize:

Hide elements, apply changes, and redisplay

The first method: hide elements, which causes two redraws while showing and hiding nodes

function appendDataToElement(appendToElement, data) {
    let li;
    for (let i = 0; i < data.length; i++) {
        li = document.createElement('li');
        li.textContent = 'text'; appendToElement.appendChild(li); }}const ul = document.getElementById('list');
ul.style.display = 'none';
appendDataToElement(ul, data);
ul.style.display = 'block';
Copy the code

The second way is to use document fragments to build a subtree outside the current DOM and copy it back into the document

const ul = document.getElementById('list');
const fragment = document.createDocumentFragment();
appendDataToElement(fragment, data);
ul.appendChild(fragment);
Copy the code

The third way is to copy the original element to a detached node, modify the node, and then replace the original element.

const ul = document.getElementById('list');
const clone = ul.cloneNode(true);
appendDataToElement(clone, data);
ul.parentNode.replaceChild(clone, ul);
Copy the code

Avoid triggering synchronous layout events

As we mentioned earlier, when we access some attributes of an element, the browser forces the queue to be emptied and the layout to be synchronized. For example, if we wanted to assign the width of an array of P tags to the width of an element, we might write code like this:

function initP() {
    for (let i = 0; i < paragraphs.length; i++) {
        paragraphs[i].style.width = box.offsetWidth + 'px'; }}Copy the code

This code looks fine, but it can cause a lot of performance problems. Each time through the loop, an offsetWidth property value of the box is read and used to update the width property of the P tag. This results in the browser having to validate the style update in the previous loop before it can respond to the style read in the current loop. Each loop forces the browser to refresh the queue. We can optimize as:

const width = box.offsetWidth;
function initP() {
    for (let i = 0; i < paragraphs.length; i++) {
        paragraphs[i].style.width = width + 'px'; }}Copy the code

For complex animation effects, use absolute positioning to keep them out of the document flow

For complex animation effects, which often cause backflow redraw, we can use absolute positioning to take it out of the document stream. Otherwise, it will cause frequent backflow of the parent element and subsequent elements. So let’s just do an example.

After opening this example, we can open the console, which will output the current frame number (although it is not accurate).

In this example, we can see that the frame count never reaches 60. At this point, as long as we click on the button and set this element to absolute positioning, we can stabilize the frame count to 60.

Css3 Hardware acceleration (GPU acceleration)

Rather than thinking about how to reduce backflow redraw, we’d like to see no backflow redraw at all. This time, CSS3 hardware acceleration on the shining debut!!

Key points: CSS3 hardware acceleration allows animations such as transform, opacity and filters to be redrawn without causing backflow. Other properties of animations, such as background-color, will still cause backflow redraw, but it can still improve the performance of those animations.

How to Use CSS3 Hardware Acceleration (GPU acceleration)

Common CSS3 attributes that trigger hardware acceleration:

  • transform
  • opacity
  • filters
  • Will-change

Css3 hardware acceleration pit

  • If you use CSS3 hardware acceleration for too many elements, it can lead to a large memory footprint and performance issues.
  • Anti-aliasing will not work on GPU rendering fonts. This is because GPU and CPU algorithms are different. So if you don’t turn hardware acceleration off at the end of the animation, the font will blur.

A few additional notes

1. What are the functions of async and defer? What’s the difference?

Let’s compare the difference between defer and async properties:

The blue line represents JavaScript loading; The red line represents JavaScript execution; The green line represents HTML parsing.

Ltscript SRC =”script.js”></script>

Without defer or Async, the browser loads and executes the specified script immediately, meaning it reads and executes document elements without waiting for them to be loaded later.

<script defer SRC =”script.js”></script>

The defer attribute represents delayed execution of the imported JavaScript, meaning that the HTML does not stop parsing when the JavaScript loads, and the two processes are parallel. After the entire Document has been parsed and deferred -script has loaded (in no particular order), all the JavaScript code loaded by deferred -script is executed and the DOMContentLoaded event is triggered.

<script async SRC =”script.js”></script>

The async property represents the JavaScript introduced by asynchronous execution, and the difference from defer is that it will start executing if it is already loaded — either at the HTML parsing stage or after DOMContentLoaded is triggered. Note that JavaScript loaded this way still blocks the load event. In other words, async-script may be executed before or after DOMContentLoaded is fired, but must be executed before Load is fired.

Defer differs from regular Script in two ways: it does not block parsing of the HTML when loading the JavaScript file, and the execution phase is deferred after parsing the HTML tags. When loading multiple JS scripts, Async loads sequentially, while defer loads sequentially.

2. Why is DOM manipulation slow

Think of DOM and JavaScript as islands, each connected by a toll bridge. — High Performance JavaScript

JS is fast, and modifying DOM objects in JS is fast. In the JS world, everything is simple and fast. But DOM manipulation is not a solo JS dance, but a collaboration between two modules.

Because DOM is something that belongs in the rendering engine, and JS is something that belongs in the JS engine. When we manipulate the DOM with JS, there is essentially “cross boundary communication” between the JS engine and the rendering engine. The implementation of this “cross-border communication” is not simple and relies on bridging interfaces as “Bridges” (see figure below).

There is a charge to cross the bridge — an expense that is not negligible in itself. Every time we manipulate the DOM, either to modify it or just to access its value, we have to bridge it. The more times you cross the bridge, the more obvious performance problems will occur. So the advice to “cut down on DOM manipulation” is not entirely groundless.

Performance optimization strategy

Based on the browser rendering principles described above, the DOM and CSSOM structure construction sequence, initialization can be used to optimize page rendering and improve page performance.

  • JS optimization:<script> The tag, along with the defer and async properties, controls the download and execution of the script without blocking page document parsing.

The defer property: Used to start a new thread to download the script file and have it execute after the document has been parsed. Async property: a new HTML5 property that is used to asynchronously download script files and immediately explain the execution code after downloading.

  • CSS optimization:<link>Setting the rel attribute to preload allows you to specify in your HTML pages which resources are needed immediately after the page loads, optimally configuring the load order and improving rendering performance

conclusion

From what has been discussed above, we may come to the conclusion that…

  • Browser workflow: Build DOM -> Build CSSOM -> Build render tree -> Layout -> Draw.
  • CSSOM blocks the rendering and only moves on to the next stage of building the rendering tree once the CSSOM is built.
  • Normally DOM and CSSOM are built in parallel, but when the browser encounters aScript tag that doesn’t defer or async, DOM building will stop. If the browser hasn’t finished downloading and building CSSOM at this point, JavaScript can modify CSSOM, So you need to wait until the CSSOM is built before executing JS, and then re-dom is built.

Reference article:

Simple browser rendering principles

Do you really understand reflux and redraw