Analysis of interface performance optimization from browser Principle 02– Interface rendering

preface

When it comes to performance optimization, interface rendering optimization is the most important thing to pay attention to. To optimize our interface rendering, we first need to understand the process of interface rendering.

Let’s start with a few common interview questions:

  • Does JS parsing block interface rendering?
  • Does loading and parsing of CSS block interface rendering?
  • Briefly describe browser redraw and reflux
  • What is GPU acceleration?
  • What are asynchronous loading and preloading?
  • What is CRP:Critical Rendering Path?

Most of the above questions are related to interface rendering in the browser. Let’s not rush to answer these questions, but let’s explore the browser rendering process and come back to these questions.

The macro architecture of the browser

Let’s start with what processes and threads are:

Process is the smallest unit of CPU allocation of resources (the smallest unit that can have resources and run independently) Thread is the smallest unit of CPU scheduling (the unit of a program running on a process)

Let’s look at the browser architecture again:

Analysis of interface performance optimization00 from browser principle

Browser kernel (browser renderer)

We often say that the browser kernel is the browser rendering process, it mainly contains JS engine threads, GUI threads, event trigger threads, etc., we first look at the work of each thread content

The GUI thread

  • GUI thread is mainly responsible for browser interface rendering,HTML file, CSS file parsing,DOM tree, rendering tree, hierarchical tree construction and so on
  • The GUI thread is also responsible for what we call the backflow, redraw process
  • GUI threads and JS threads are mutually exclusive, and JS threads are suspended while GUI threads are running and vice versa

JS engine thread

  • The JS engine thread, or JS kernel, is responsible for processing JS scripts,
  • JS thread is also responsible for JS garbage collection
  • JS is also responsible for waiting for tasks in the task queue to arrive and then processing them
  • JS threads are single threaded
  • JS threads and GUI threads are mutually exclusive, and the GUI thread is suspended while the JS thread is running and vice versa

Event trigger thread

  • The event triggering thread is responsible for maintaining an event queue
  • Macro tasks are added to the end of the event queue when our actions or JS scripts generate them

It must be a trigger thread

  • Handles setTimeout, setImmediate and other timers
  • This is because our JS thread parses the code first and then executes it line by line, because the accuracy of the timer may be affected if the JS thread blocks. So we need to use a separate thread to handle the timer work
  • Note that setTimeout in the browser has a minimum trigger interval of 4ms

Asynchronous HTTP request threads

  • Open a new thread through the browser after the XMLHttpRequest connection
  • When a state change is detected, the callback function is placed in an asynchronous queue waiting for the JS thread to execute

The browser’s rendering flow

Let’s take a look at the rendering process of the browser interface. This process takes place after the HTTP request receives the data. First, we need to establish a clear understanding in advance: We’re going to start with the HTTP request, and it’s not just one step and then the next, it’s an assembly line and with that in mind, we’re going to go on to the next article

Analysis of interface performance optimization from browser principles – Web browser requests

Then, look at what happens after the browser:

Prepare the render process

When the content-type of our web request’s response header is text/ HTML, the browser will proceed with the navigation process and create our rendering process. By default, browsers assign one render process to each TAB page, but in some cases browsers may assign multiple interfaces belonging to the same domain name to the same render process. Assigning multiple pages to the same process is called the same site policy. Here we can understand what the same origin site is:

We call the same site the root domain (demo.com) and protocol (HTTPS or HTTP), as well as all the subdomains and different ports under the root domain. Such as:

  • www.demo.com
  • time.demo.com
  • www.demo.com:8081 They are both the same site because their protocol is HTTPS and their root domain name is demo.com

Render process strategy

The render process strategy we used when we opened the new screen was:

  • A new rendering process is created when the new screen is opened normally
  • If the newly opened interface is the same site as the existing interface, the previous renderer process will be reused
  • If the new page and the previous page do not belong to the same source site, a new process is created

Submit the document

At this stage, the browser submits the HTML data received by the web process to the renderer:

  • After receiving the response header data from the web process, the browser process sends a message to the renderer to submit the document
  • The renderer process, after receiving the message to submit the document, establishes a transmission data pipeline with the network process
  • After the document data has been transferred, the renderer submits and sends a message confirming the submission to the browser process
  • After receiving the confirmation message, the browser process updates the interface, including the URL of the address bar, the status of the rollback bar, and the Web interface

PS: Note that we said earlier that the rendering process is an assembly-line state. So we have simplified the process of submitting documents just for the sake of description

Rendering phase

When the renderer receives the submission message, it begins the process of interface parsing. Here is a summary of the process:

  • Parse HTML files, build DOM trees, and download and parse CSS
  • After the CSS is parsed into a CSS Tree, the DOM Tree is combined into a Layout Tree
  • Layout Tree confirms the size and position information of interface elements (the redrawing process takes place here) and draws the pixel information of elements (the redrawing process takes place here).
  • Layout Tree Generates a hierarchical Tree after obtaining the information of each interface element
  • Hierarchical tree: The drawing of bitmap begins after the block processing of each layer (this step is completed in GPU)
  • After the bitmap is drawn, notify the browser process to display the Web interface

Parsing the HTML generates a DOM tree

First, why build HTML as a DOM tree? Because HTML exists as a string in the browser and is not recognized by the browser, it needs to be converted into a knot that the browser can understand: a DOM tree

Let’s take a look at the structure of the DOM tree, which shamelessly uses images from other articles on the web:

The above procedure is roughly the process of converting an HTML file into a DOM tree through an HTML parser. As we all know,HTML is called hypertext Markup Language (HTML), for example:

<p>content</p>
Copy the code

The “P” tags in code are called tags, and the “content” is called text, meaning that HTML is made up of text and tags. PS: To reiterate the previous point: browser rendering is a pipelined state, so our HTML parsing is as much data as it receives from the web process. Let’s briefly explore the process of HTML generating a DOM tree:

First of all, our general flow chart is as follows:

Byte stream is converted to token

In this stage, the word divider plays a major role. The word divider will change byte flow into tokens one by one, which can be simply divided into Tag tokens and text tokens. For example, the following code:

<html>
  <body>
    <div>test</div>
  </body>
</html>
Copy the code

We can divide the above code into the following tokens by means of a tokenizer

In the figure above, we can see that the Start Tag Token corresponds to the End Tag Token one by one, and the text Token is a separate Tag.

The Token is converted to a DOM node, and the DOM node is added to the DOM tree

This is actually a two-step process of converting tokens into DOM nodes and DOM nodes being added to the DOM tree, but as we mentioned earlier, it’s a pipeline-like process and we combine it together for ease of understanding. To convert a Token into a DOM tree, a Token stack needs to be maintained. The conversion process is as follows

  • The first step is to generate a document and node and push a Document Start Token
  • Then we process the tokens that are resolved by our tokenizer
  • If it is a Start Token, it is pushed to create a DOM node and mount it to the DOM tree
  • If it is an End Token and the top of the stack is a corresponding End Token, then the top Token is removed from the stack
  • If the tokener parses a text Token, a text node is generated and mounted to the DOM tree (note: the text Token is not pushed; its parent is the currently mounted DOM node).
  • When all of our byte flow lexicon is parsed and the Token stack is empty, our DOM tree is built

Style calculation

DOM tree generation requires both CSS and HTML, which means that style calculation takes place before the DOM tree is fully generated.

Style calculation is mainly to parse CSS and calculate the specific style of DOM nodes. This stage can be roughly divided into three stages

Conversion of CSS

As we all know, our CSS styles come from three main sources:

  • External import through the link label
  • The inline style of the style tag
  • The inline style of the element

These styles are also stored in the browser as byte streams before being parsed. When the rendering engine receives THE CSS text, it performs a transformation to convert the byte stream into a structure that the browser recognizes :StyleSheets.

Normalized attribute value

The main purpose of this step is to convert the various CSS styles into standardized values that the rendering engine can understand. Such as:

This process is called attribute value normalization

Calculate the specific style of the DOM node

This step basically calculates the style of the DOM node and appends it to the corresponding node. Here style calculation uses inheritance rules and cascading rules:

  • Inheritance rules: Each DOM node contains the style of its parent node.
  • Cascade rules: Style cascade specifies how algorithms merge attribute values from multiple sources. For those of you who are interested, look up cascading style sheets

The layout phase

We now have the DOM tree and DOM node styles, but the browser is missing one key point: the location of each element. The rendering engine then needs to calculate the geometry of each visible element, a process called layout.

This stage requires two steps: creating the layout tree and computing the layout

Create a Layout Tree

The main purpose of a layout tree is to build visible elements into a tree. It basically contains the following two contents:

  • Iterate through all the nodes in the DOM tree, and then add the visible nodes to the layout tree
  • Invisible nodes are ignored
Layout calculation

The main function of this stage is to calculate the geometric position information of each element and store it in the layout tree.

layered

The layering phase is done on the basis of the layout tree. There are many complex effects in the interface, and in order to facilitate the implementation of these effects, the rendering engine needs to generate a special layer for a particular node and a corresponding layer tree. The layer tree here is analogous to the concept of hierarchical context that we learned.

As we can see from the above description, the browser page is actually divided into several layers, which are superimposed to form the final interface.

PS: Not every node belongs to a layer. If a node has no corresponding layer, it belongs to the parent node’s layer.

A separate layer will be promoted if the following rules are met:

  • Elements that have the cascading context attribute are promoted to separate layers
  • Areas that need to be cropped will also be created as layers

Layer to draw

Once the layer tree is built, the rendering engine will draw each layer. The process is actually quite simple. Suppose we draw a red rectangle with a yellow circle on a black background. So our drawing process can be roughly described as: first draw a black background, then draw a rectangle according to the position information, and then draw a yellow circle according to the position information of the circle. The rendering engine’s rendering process is similar in that it breaks down the layer drawing into instructions, which are then combined into a drawing list.

The draw list is only used to describe the drawing information of layer elements, but the actual drawing is done by the composition thread with the rendering engine.

block

Once the drawing list is complete, it is handed over to the composition thread to draw. First of all, we need to clarify the concept that the page of a web page may be large, but our users only see part of it, which we call the viewport. For these reasons, the composite thread splits the interface into chunks. The block size is usually 256,256 or 512,512. The composite thread will generate bitmaps of blocks near the viewport first.

rasterizer

The actual operation of generating bitmaps is performed by rasterization also known as rasterization, and the graph block is the smallest unit to perform the rasterization operation. Rasterization is done in a rasterized thread pool, which is maintained by the rendering engine.

Generally, the rasterization operation will be completed by GPU acceleration. The operation of GPU to generate bitmaps is also called fast rasterization or GPU rasterization, and the generated bitmaps will be stored in the GPU. PS: Remember that the browser is divided into a rendering process and a GPU process, which means that our rapid rasterization process involves cross-process communication

Composition and display

Once all the tiles are rasterized, the composite thread generates a command to draw the tiles: “DrawQuad” and submits the command to the browser process. A component in the browser called Viz receives a “DrawQuad” command from the composite thread, draws the contents of the corresponding command into memory based on that command, and finally displays the in-memory interface on the screen.

At this point, after a series of steps, we are finally able to see our interface in the browser.

Answer key

1. Does JS parsing block interface rendering

The JS thread and render thread are mutually exclusive, and when one executes, the other is suspended until the other completes. From this we can draw a conclusion :JS parsing will block the rendering of the interface JS loading situation is complicated, let’s discuss separately:

First, insert a javascript script in the middle of the HTML:

<html>
  <body>
    <div id="test">test1</div>
    <script>
      let div1 = document.getElementsById('test')
      div1.innerText = 'test'
    </script>
    <div>test2</div>
  </body>
</html>
Copy the code

The previous HTML parsing is the same as we mentioned before, but after the script tag is reached, the rendering engine is temporarily suspended and the JS script is executed. After the script is executed, the content of the div with the id “test” is changed to “test”. The rendering engine then resumes rendering the remaining HTML text. Now that we understand the rendering flow above, let’s look at a similar one:

Introverted JS scripts are replaced with JS imported files
<html>
  <body>
    <div>1</div>
    <script type="text/javascript" src="foo.js"></script>
    <div>test</div>
  </body>
</html>
Copy the code

The script script is replaced with an externally imported file, but the content is exactly the same. But unlike before, JS external scripts need to be downloaded, and this JS download process also blocks DOM parsing. However, the browser does an optimization called preparsing: when the browser receives the HTML stream, it opens a preparsing thread that parses the JS and CSS files contained therein. The preparsing thread downloads the files after they are parsed to the relevant files. Therefore,JS downloading and parsing will block DOM parsing, and we can reduce the blocking time by using CDN acceleration, Async /defer, preload, and other strategies.

The third case

Here’s a look at what happens when CSS is added:

<html>
  <head>
    <style src="theme.css"></style>
    <style>
      div {
        color: blue;
      }
    </style>
  </head>
  <body>
    <div>1</div>
    <script>
      let div1 = document.getElementsByTagName('div') [0]
      div1.innerText = 'time.geekbang' / / to the DOM
      div1.style.color = 'red' / / need CSSOM
    </script>
    <div>test</div>
  </body>
</html>
Copy the code

We can see that CSS styles are operated in THE JS script, so before executing the JS script, we need to wait until the CSS is loaded and resolved into Style Sheets. So CSS blocks the execution of JS scripts in this case.

2. Does loading and parsing of CSS block interface rendering

From the above browser rendering process, we can see that the Layout Tree is constructed by HTML DOM and Style Sheets. And our CSS loading is an asynchronous process, so CSS loading does not block DOM building. However, the Layout Tree is built by combining THE HTML DOM and Style Sheets, so loading CSS blocks the rendering of the interface.

At the same time, we know that JS can manipulate the DOM and CSS styles of the interface, so the browser needs CSS loading and parsing to complete when parsing JS, and the JS thread and rendering thread are mutually exclusive. That is, DOM rendering is blocked when parsing JS, and CSS stylesheets are required for JS parsing. This means that CSS can block DOM rendering by blocking JS parsing

3. Briefly describe browser redraw and reflux

As we can see from the above description, the layout phase of the browser rendering process includes

  • Layout Tree building
  • Determine the geometry of an element (this process will backflow)
  • Determine the pixel information of an element (redraw triggers this process)

Therefore, we can have a clear understanding: backflow must cause redraw, redraw does not cause backflow

There are many operations that trigger backflow and redraw, which can be roughly divided into the following two categories:

  • Actions that affect the position of an element in the document flow and the geometry of an element result in a backflow operation
  • Operations that affect the pixel information of the element itself cause a redraw operation

PS: When we get the geometry or location of an element, it also causes backflow because the browser has to give us the exact information

4. What is GPU acceleration

As we can see from the browser rendering process above,GPU acceleration skips backflow and redraw and performs layer drawing and composition operations. Therefore, if we want to use GPU acceleration, we need to upgrade our element change operation to the synthesis layer. There are many operations that can be upgraded to the synthesis layer. Here are a few:

  • The transform property in the CSS
  • Animates CSS on opacity
  • Will-change of CSS (use with caution, as there are many browser optimizations and this method does not improve performance much)
  • Implicit composition of CSS

5. What are asynchronous loading and preloading

Asynchronous loading :async/defer

Asynchronous loading can change our JS script download to asynchronous download, which means that the JS download process does not block our DOM rendering. But note the difference between Async and defer:

  • Async is unordered and executes as soon as the JS script is downloaded
  • Defer is in order. Once the JS script has been downloaded, it will execute the script in the loading order before DOMContentLoaded

As can be seen from the above differences, JS scripts are not dependent on unordered async because of their modularity.

preload

Preload /prefetch, both of which are REL attributes in the link tag, with the difference

  • Preload provides declarative commands that let the browser load resources ahead of time and execute them when needed
  • Prefetch is a resource that tells the browser that the next page will be used, which means prefetch is there to speed up the next interface

In simple terms, preload tells the browser what resources must be preloaded, and prefetch tells the browser what resources can be preloaded. Both preload and prefetch take precedence over Prefetch

6. What are the key render paths

The Critical Rendering Path (CRP) is the set of steps that you go through to render the first screen of a browser as we discussed above. It contains the following three parts:

  • Number of critical resources: resources that block the first rendering
  • Critical path length: the round-trip time required to obtain all critical resources
  • Key bytes: The total number of bytes required for the first screen rendering, equal to the sum of all key resource files

The front-end optimization

With all that said, we finally get back to our main topic: front-end performance optimization. I think you might be a little tired. So, here’s a quick summary of the browser rendering process:

  • Parse HTML files, build DOM trees, and download and parse CSS
  • After the CSS is parsed into a CSS Tree, the DOM Tree is combined into a Layout Tree
  • Layout Tree confirms the size and position information of interface elements (the redrawing process takes place here) and draws the pixel information of elements (the redrawing process takes place here).
  • Layout Tree Generates a hierarchical Tree after obtaining the information of each interface element
  • Hierarchical tree: The drawing of bitmap begins after the block processing of each layer (this step is completed in GPU)
  • After the bitmap is drawn, notify the browser process to display the Web interface

Our front-end performance optimization can follow the above steps:

CSS optimization

The first thing we need to know about CSS optimization is that CSS matches are made from right to left. For example:

#list li{}Copy the code

The conventional thinking would have been that the rendering engine would have matched “#list” and then looked under it for the “li” tag, but in fact the opposite was true: the rendering engine would have had to iterate over every Li element and then find out if its parent element id was “list”, which would have been a time-consuming operation. We’re looking at one that we’ve used before:

* {}Copy the code

Previously we might have used wildcards to clear styles, but wildcards traverse all elements, so they can be very expensive, so avoid them

To sum up, we can have the following performance optimization solutions when writing CSS:

  • Try to avoid using wildcards
  • Avoid label selectors because they traverse all corresponding labels in the document
  • If you can use inherited styles, use them to avoid multiple computations
  • Minimize unnecessary nesting, which can cause multiple traversals and increase overhead
  • Use the ID and class selectors as concisely as possible, and try not to pair them with other tags

Render blocking optimization

From the previous introduction, we can know that in the process of page rendering, unreasonable JS and CSS loading will block the interface rendering. Therefore, what we need to do is to arrange the loading order of scripts properly to avoid blocking the interface rendering. The main points are as follows:

  • CSS is the resource blocking rendering, we should download as early as possible, for example, put CSS in the head tag, and start the optimization of loading speed of CDN static resources
  • The JS script loads late, because for the first screen, our interface rendering won’t be affected much without JS, so we can either put the JS script at the end of the document, or use Async /defer to load it asynchronously

Reduce DOM modification times

This is a problem about inter-thread communication. As we know from the above introduction,JS thread and rendering thread are different threads, so when we modify the DOM through JS, inter-thread communication will also cause extra overhead. To avoid this unnecessary overhead, we can combine multiple DOM operations into one, using a typical example:

let wrapper = document.getElementsById('demo')
let fragment = document.createDocumentFragment()
for (let i = 0; i < 10; i++) {
  let p = document.createElement('p')
  p.innerText = ` test${i}`
  fragment.appendChild(p)
}
wrapper.appendChild(fragment)
Copy the code

The above operation takes advantage of the Fragment’s nature to merge ten DOM updates into one

Optimization of reflux and redraw

As you can see from the above, the browser can be too expensive to backflow and redraw when manipulating the DOM, so the best thing to do in this case is to avoid backflow and redraw. To avoid this, we first need to look at all the operations that can cause backflow and redraw:

The operation that causes backflow

  • Change the geometry of the DOM
  • Changing the structure of the DOM tree (that is, changing the position of elements in the document flow)
  • Gets some specific attribute values. Such as offsetTop, offsetLeft, offsetWidth, and offsetHeight.

As an added bonus, the browser has been optimized for backflow operations to maintain an update queue that will be emptied after a certain amount of time or when we get the geometry of the element to perform DOM operations. Therefore, we should avoid frequent reading and writing of elements’ geometric position attributes to avoid layout jitter.

Causes the redraw operation

When we modify the pixel information of an element, we will trigger redraw operations, such as modifying the color,backgroundColor and other attributes.

How to avoid backflow and redraw

  • Instead of changing styles line by line, merge style changes using class names
  • To strip the document flow of the DOM to be modified, we can modify the display: None property of the DOM to be continuously modified for multiple times. Although this will cause backflow operation, it avoids layout jitter caused by multiple modifications
  • Directly bypassing the reflow and redraw to elevate elements to compositing layers, as mentioned above in the GPU acceleration section.
  • Cleverly used, micro-tasks are executed before the next browser frame refreshes, eliminating multiple browser refreshes

conclusion

With this introduction, we have outlined a general process of browser rendering, and we can also follow this process to perform some performance optimizations at the rendering level. Due to the lack of space, there is no way to expand in detail, so I can only provide some ideas for rendering optimization. This is just a bit of a kick in the ass, and I hope to see the gods here, if they are interested, come up with their own suggestions or opinions. I would be grateful if you could help me fill in the gaps in my knowledge.