Deep understanding of browser rendering

Long warned

preface

Fast loading of page content and smooth interaction are what users want from the Web experience, so we front-end developers should strive for both. Understanding how browsers work can help you optimize web page performance. This article goes into the details and steps of rendering a web page using a browser.

1. Javascript engine in browser environment

Take Chrome for example. Chrome has a built-in V8 engine for parsing JavaScript code. Every time we open a web page, we open a browser process, and there are many threads in the process. This is also a well-known feature of JavaScript’s parser — single-threaded. That is, it can only do one thing at a time, and if it gets blocked, it can’t do anything else.

Block and asynchronous tasks

In the browser, we smooth implementation of the JS code is very important, a single thread of JS code once spent a long time the task of jam, web will be a big problem, so has the design of the asynchronous task in JavaScript, when you need to complete some tasks need results in a moment, you can add it to the asynchronous queue, This way it won’t block subsequent execution of your JavaScript code. For details on how JavaScript works, see my article on JavaScript event loops.

3, the browser parse web page related process

We know that the browser is a process, which runs many threads, such as plug-ins, UI, user data, bookmarks, etc., we do not need to consider so many additional threads, in the rendering process, the core is the following:

  • Browser GUI rendering thread

    The main function of this thread is to parse HTML documents, parse CSS documents, build DOM trees and CSSOM trees, and call their two composite Render trees to Render content. It is also responsible for the backflow and repainting of the page so that the pixels are actually rendered on the page.

  • JS parsing thread

    Mainly responsible for parsing JavaScript scripts, it will constantly wait for the arrival of new tasks in the ready task queue; There is only one Js parsing thread in a browser process.

  • The network thread

    Responsible for the network to request needed resources, such as external chain JS, CSS files, as well as picture font resources. Each new network request opens a network thread.

We all know that the operation of the JS is can change the DOM structure and style, in order to avoid just finished rendering DOM is JS modified again to render the performance of this waste happens, GUI rendering process at the time of JS parser execute JavaScript, will be blocked, until task queue JS engine idle work will continue to parse rendering. This means that the GUI rendering thread and the JS parsing thread are mutually exclusive.

4. Key render paths

Now let’s take a look at the steps the browser takes to render the HTML, CSS, and JavaScript into a page. The entire rendering process is shown below:

4.1. Build a DOM tree

When we get the returned HTML file from the server, the browser parses the HTML code and converts it into a DOM tree, where each node holds the information we need.

4.2. Build the CSSOM tree

The CSS Style file is parsed and the CSSOM tree is generated, with nodes that store Computed Style information, namely Computed Style, after the CSS rules are resolved by the corresponding node.

4.3 Render Tree synthesis

Once we have the DOM tree and the CSSOM tree, we combine them to get the render tree.

4.4. Layout

Once the render tree is built, the layout becomes possible. The layout depends on the screen size. The layout step determines where and how to place elements on the page, determines the width and height of each element, and the correlation between them. Thus, according to the render tree, the layout is carried out to get the geometric information of nodes on the screen.

4.5. Repainting

The final step is to draw the pixels onto the screen. Once the render tree is created and the layout is complete, the pixels can be drawn on the screen.

Resolve blocking in rendering

In normal web pages, the above key render path is not likely to be directly completed, because in the above process, we do not take into account the JS parsing thread and GUI rendering thread mutually exclusive, when parsing JS, the key render path will be stopped. So, I’m going to talk about blocking in rendering.

5.1. JS executes blocking DOM build rendering

Since browsers assume the worst for JS execution, the GUI rendering thread blocks while we are working on the JS parsing thread, waiting for JS execution to complete before continuing. HTLM parsing is also the job of the GUI rendering thread, which means that the browser will pause parsing of THE HTML and building of the DOM tree while JS executes.

This is why it is often recommended to put JS at the bottom of the body, one of the reasons is not to block the building of the DOM tree. This is why JS on top of the body often fails to get the desired DOM node because the building of the DOM tree is blocked before completion.

However, putting JS directly at the bottom can be a bit of a problem. Putting JS directly at the bottom means that it will be parsed and loaded last. If your page relies on JS to render some content, it will cause the full rendering of the page to be delayed.

5.2. JS loading blocks all

Unlike loading external CSS files, loading external CSS files does not block the parsing of HTML and the building of the DOM tree (the loading file is handed over to the network thread), and at most slows down the first rendering (the rendering will not take place without the CSS), but loading external JS files is different. The browser doesn’t know if the code is modifying the already built DOM node, so it assumes the worst and waits for the JS file to load -> run before continuing to parse the HTML.

5.3. CSS blocks DOM tree rendering

In 4, we learned that the construction of CSSOM in the key rendering path is a critical step, that is, when the construction of CSSOM is not completed, even if the DOM tree has been built, the browser will not render the DOM tree, but from the figure in 4, we can see that the construction of CSS and HTML parsing can be parallel. So the parse build of CSS does not block the build of the DOM tree, but, as mentioned in 5.2, slows down the first rendering of the DOM tree.

So in order to get the page rendered as quickly as possible, we should provide the CSS we need as soon as possible.

5.4. CSS parsing builds block JS execution

We all know that JavaScript is so powerful that they can read and modify not only DOM properties, but CSSOM properties as well. What happens when CSS builds CSSOM and JS modifs the CSSOM? Obviously, we now have a race problem.

What if the browser hasn’t finished downloading and building CSSOM, and we want to run the script at this point? The simple answer is bad for performance: the browser will delay script execution until it has been downloaded and built by CSSOM.

For a simple example, look at this code:

<! DOCTYPEhtml>
<html>
<head>
  <title>test</title>
  <script>
    let startDate = new Date(a)document.addEventListener('DOMContentLoaded'.function() {
      console.log('DOMContentLoaded');
    })
  </script>
  <link href="https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css" rel="stylesheet">
  <script>
    let endDate = new Date(a)console.log('run time:' + (endDate -startDate) + 'ms')
  </script>
</head>
<body>
  <h1>test</h1>
</body>
</html>
Copy the code

When the browser renders a page, it triggers two events, DOMContentLoaded, which means that the DOM tree has been built, and onLoad, which means that the page has been loaded, including CSS, JS, and images.

We run the page in our browser and the result is as follows:

As you can see, the JS script located behind the CSS resource will not resume execution until the CSS resource is loaded, and the execution of JS will block the DOM tree construction. Therefore, the DOM tree is not completed until the completion of JS execution, and the DOMContentLoaded event is triggered.

6. Preload Preloading

Above I described some of the sequential adjustments that browsers make for performance purposes when loading resources on a page (using blocking to achieve sequential execution). But sometimes, there are resources that our web page needs right away. Consider this example:

<! DOCTYPEhtml>
<html>
<head>
  <script src="/js/index.js"></script>
  <script src="/js/menu.js"></script>
  <script src="/js/main.js"></script>
</head>
<body>
  <div id=app></div>
</body>
</html>
Copy the code

When we get to index.js d, we will load it. This is the case in 5.2. Loading js will stop parsing HTML and DOM construction, so the browser will have to wait for index.js to finish loading before it can continue parsing. Now let’s analyze the situation of three main threads when JS is loaded and executed:

  • GUI: blocked
  • JS parsing thread: the code that has just been loaded is being executed
  • Network thread: idle

Obviously the network is free when parsing executing JS, so there is room to play: can we parse executing JS/CSS while requesting the next (or next batch) resource?

For this case, the browser provides a preload mechanism, preload, to quote MDN’s explanation:

The rel attribute of the Link element preload allows you to write declarative resource requests inside the head element of your HTML page that specify which resources are needed immediately after the page loads. For this immediate-need resource, you may want to acquire it early in the page loading lifecycle, preloading it before the browser’s main rendering mechanism kicks in. This mechanism allows resources to be loaded and available earlier and is less likely to block the initial rendering of the page, thus improving performance.

Let’s change the above code to something like this:

<! DOCTYPEhtml>
<html>
<head>
  <script src="/js/index.js"></script>
  <link src="/js/menu.js" rel=preload as=script></link>
  <link src="/js/main.js" rel=preload as=script></link>
</head>
<body>
  <div id=app></div>
</body>
</html>
Copy the code

Now the page can parse index.js and continue loading the next two JS resources, and this method does not execute, that is, it does not block HTML parsing and DOM tree construction. Instead, it puts the loaded resources into memory. Then we use the method described in 5.1 to place the js execution at the bottom of the body:

<! DOCTYPEhtml>
<html>
<head>
  <script src="/js/index.js"></script>
  <link src="/js/menu.js" rel=preload as=script></link>
  <link src="/js/main.js" rel=preload as=script></link>
</head>
<body>
  <div id=app></div>
  <script src="/js/menu.js"></script>
  <script src="/js/main.js"></script>
</body>
</html>
Copy the code

In this way, when the DOM tree is built and the JS at the bottom is parsed, the cache content that has already been loaded can be directly executed without waiting for the LOADING of JS. In this way, the problem of putting JS at the bottom of the body mentioned in 5.1 can be effectively solved.

7. Prefetch Prefetch resources

The browser also provides a value for the REL attribute of the link tag, called prefetch. Again, take the explanation from MDN:

Prefetch is a browser mechanism that uses idle browser time to download or prefetch documents that the user may access in the near future. The Web page provides a set of prefetch prompts to the browser and silently starts pulling the specified document and storing it in the cache after the browser finishes loading the current page. When the user accesses one of the prefetched documents, it can be quickly retrieved from the browser cache.

Let me give you a simple example of a real world scenario – lazy loading of images.

When we’re browsing the web, images that don’t make it into our window tend to load lazily to reduce the stress of first loading. When we finish loading the page and don’t scroll down, the browser is already idle. Normally, we wait until the image below is in view before loading, but we can wait until the page is free to load the image below. Lazily loaded images can be read from the cache quickly when the user scrolls down, greatly improving the experience.

This is called resource prefetch, and it does not take up the network resources at the time of the first load, that is, when the browser is idle to load some resources that the user may need in the future, as MDN says. So resource prefetching does not affect the first rendering, it is triggered a bit before the onLoad event.

8. Preload the scanner

Preload is preloaded only when the browser knows that the link has preload attributes, that is, the line is parsed.

However, as we mentioned in 5.2, loading JS blocks HTML parsing. Let’s go back to the example in the preload section:

<! DOCTYPEhtml>
<html>
<head>
  <script src="/js/index.js"></script>
  <link src="/js/menu.js" rel=preload as=script></link>
  <link src="/js/main.js" rel=preload as=script></link>
</head>
<body>
  <div id=app></div>
</body>
</html>
Copy the code

I mentioned in the preload section that the following preload resources can be loaded when parsing index.js, but since js parsing blocks HTML parsing, the browser should not know what the following link tag is.

In the actual web page, we can open network to observe the loading of resources, and we can also find that some resource loading after the loading of JS can also be resolved by the browser to send requests at the same time, which seems to be in contradiction with what we said in 5.2.

But this is the browser’s own optimization mechanism, rather than the HTML file being parsed by the browser as soon as it is downloaded from the server:

As you can clearly see from the timeline, some time has passed between the HTML file being downloaded and the first resource being loaded, and this time is caused by the scanner being preloaded.

To quote MDN:

This process takes up the main thread when the browser builds the DOM tree. When this happens, the preload scanner will parse the available content and request high-priority resources, such as CSS, JavaScript, and Web fonts. Thanks to the preloaded scanner, we don’t have to wait until the parser finds a reference to an external resource to request it. It retrieves the resources in the background so that by the time the main HTML parser reaches the requested resources, they may already be running or have been downloaded. The optimizations provided by preloading scanners reduce congestion.

Know that we can well understand why JS load blocks of HTML parsing, but the following resource request can be sent, preloading device to help us advance scanned the request of the external resources, so the example above index. The JS was preload preload early behind the parser, can, of course, the normal loading in advance.

conclusion

This article is summarized from the browser rendering principle set after I many, and probably a lot of articles about various aspects, but there is no unified together, and there are very few talk about loader this article, the middle for a long time cause I can’t understand why the browser can parse in advance to the JS loaded at the back of the external resources. Hopefully this article will help you improve your web performance.

Original address -> my blog

reference

Juejin. Cn/post / 684490…

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

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

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

Developers.google.com/web/fundame…