iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🎨

How Browsers Render Web Pages: A Summary

に公開

Sometimes I find myself wanting to check the performance of a web app and open the Performance tab in Chrome DevTools. Looking at the Network panel, all I can see is that various resources are being fetched, and looking at the main thread panel, I only see that various processes are being executed. Even if I don't know how to optimize performance yet, I at least—at the very least—want to have some idea of what is happening.

Upon investigation, I found that the Critical Rendering Path, the mechanism by which browsers display web pages, is crucial. From an optimization perspective, it seems relevant not only to the initial page load but also to how snappy the web app feels during subsequent user interactions.

This post is a summary of the mechanism by which browsers display web pages, written for my own understanding.

Critical Rendering Path

The Critical Rendering Path is the sequence of steps that converts HTML, CSS, and JavaScript into pixels displayed on the screen. The Critical Rendering Path consists of the following steps:

  1. DOM Construction
  2. CSSOM Construction
  3. Style (Render Tree Construction)
  4. Layout
  5. Paint + Composite

The construction of the DOM and CSSOM is referred to as parsing, and the subsequent processes are called rendering.

Optimizing the Critical Rendering Path shortens the initial page load time and improves the responsiveness to subsequent user operations. Since the Critical Rendering Path represents the series of steps from receiving HTML from the web server to finally displaying the web page on the screen, optimization can reduce initial load times. Furthermore, because steps on the Critical Rendering Path are executed when the screen is updated due to user operations, responsiveness is also enhanced.

Additionally, since rendering is executed on the main thread, if these processes take too long, JavaScript execution may be delayed, potentially leading to poor responsiveness.

Below is a diagram showing an overview of the Critical Rendering Path.

Overview

JavaScript execution refers to the running of JavaScript programs that modify the DOM or CSSOM. Although it is not explicitly one of the steps in the Critical Rendering Path, it can occur during DOM construction if script tags are present in the HTML, or as part of event handling.

From here, I will summarize each step of the Critical Rendering Path.

1. DOM Construction

DOM construction is the step of building data called DOM (Document Object Model) from the received HTML; it is the HTML parsing process. DOM is internal data representing an HTML or XML document, which can be accessed and manipulated via programs. When a browser receives HTML from a web server or other sources, it parses the HTML and builds the DOM.

Basically, the DOM does not contain style information; data that handles styles is the CSSOM, which will be described later. One exception is that style information set via the HTML style attribute is stored in the DOM.

DOM construction can be performed incrementally, so the DOM can be built even from partial HTML. Browsers process HTML using a streaming method, so partial data may be passed. Since DOM construction can be done incrementally, the process can start as soon as fragments of HTML are received.

As an example of the DOM, given the following HTML:

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link href="style.css" rel="stylesheet" />
    <title>Critical Path</title>
  </head>
  <body>
    <p>Hello, <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg" /></div>
  </body>
</html>

the following DOM is constructed.

DOM

Resource Loading

When resources are detected during HTML parsing for DOM construction, they are loaded. Examples of resources include images, fonts, CSS, and JavaScript.

Among these resources are those called block resources, which block the critical rendering path. Block resources include render-blocking resources, which block rendering, and parser-blocking resources, which block HTML parsing. Resources such as images and fonts are not block resources, and their loading is not waited upon.

Render-blocking resources include <link rel="stylesheet"> tags that do not have the disable attribute or device-specific media attributes. When the parser detects such a resource, it waits to render until the CSS is loaded and the CSSOM (described later) is constructed. On the other hand, the HTML parser is not blocked by the CSS loading and can continue parsing the rest of the HTML in parallel to construct the DOM. Rendering is executed when both the DOM and CSSOM are ready.

CSS is generally a render-blocking resource to prevent a phenomenon called FOUC (Flash of Unstyled Content), where an unstyled page is momentarily displayed. If CSS were not a render-blocking resource, it means there would be a possibility that the screen is rendered using only the DOM without style information before the CSS is loaded and the CSSOM is constructed.

Parser-blocking resources include <script> tags that do not have the defer or async attributes, or type="module". Unlike render-blocking resources, these block the HTML parser due to the loading of the resource. Once the resource is loaded, the script is executed, and the parser resumes DOM construction only after waiting for its completion.

JavaScript is essentially a parser-blocking resource because it can modify the DOM and CSSOM. I could not find precise information on why the ability to modify the DOM or CSSOM requires it to be a parser-blocking resource, but when I asked ChatGPT, I received the answer: "to maintain HTML consistency and a predictable parsing order."

Parser-blocking resources are also effectively render-blocking resources. Since rendering occurs after parsing in the Critical Rendering Path, if parsing is blocked, it can be said that rendering is also blocked.

In <script> tags, using the defer attribute, async attribute, or type="module" ensures that the parser is not blocked during script loading. With defer, the script is executed just before the DOMContentLoaded event after DOM construction. With async, the script is loaded asynchronously and executed as soon as the loading is complete. type="module" behaves similarly to defer, and using type="module" with async results in behavior similar to async.

While the defer attribute executes the script just before the DOMContentLoaded event after DOM construction and thus does not block the HTML parser during script execution, the async attribute may interrupt the HTML parser during DOM construction because it executes the script immediately after loading.

The issue of delayed resource detection while a parser-blocking resource is blocking the parser can be mitigated by a secondary HTML parser called a preload scanner. The preload scanner works in parallel with the main HTML parser to load detected resources on the spot. While the main HTML parser is blocked by JavaScript processing, the preload scanner can detect and load resources further down the document. This increases the likelihood that a resource will already be loaded by the time the main HTML parser reaches it.

However, since the preload scanner does not execute JavaScript, it cannot detect resources such as dynamically inserted script tags.

2. CSSOM Construction

CSSOM construction is the step of building data called CSSOM (CSS Object Model) from CSS; it is the CSS parsing process. CSSOM is a tree-like data structure containing all CSS selectors and the properties associated with those selectors.

Unlike the DOM, CSSOM construction is not performed incrementally. Because CSS rules can be overridden, the CSSOM is not complete until all CSS has been parsed. For example, given the CSS .foo { color: red; } and .foo.bar { color: blue; }, an element with both foo and bar classes would have the .foo {} style applied and then overridden by the .foo.bar {} style, resulting in color: blue;.

Therefore, as explained in Resource Loading, rendering is blocked until all detected CSS has been loaded and processed. In addition to preventing FOUC, this also avoids the possibility of displaying incorrect styles due to partial CSS application.

As an example of CSSOM, given the following CSS:

body {
  font-size: 16px;
}

p {
  font-weight: bold;
}

span {
  color: red;
}

p span {
  display: none;
}

img {
  float: right;
}

the following CSSOM is constructed.

CSSOM

The reason CSSOM has a tree structure is to efficiently perform style inheritance based on parent-child relationships. In CSS, rules need to be propagated from parents to children, and a tree structure allows parent node styles to be automatically propagated to child nodes. In the diagram above, the styles written in light text are those inherited from ancestors. The p span {} CSS inherits font-size and font-weight properties from its ancestors.

Additionally, the diagram above is not a complete CSSOM; it only shows the styles determined to be overridden in the stylesheet. In reality, it includes styles retrieved from the default user-agent stylesheet that exists for each browser.

3. Style

Style is the step of constructing the render tree from the DOM and CSSOM. Since the DOM and CSSOM built so far are both independently managed data structures, constructing a render tree from these two simplifies subsequent processing. The render tree contains only the nodes necessary for rendering.

The render tree is built by traversing from the root of the DOM tree, excluding nodes that are not related to display or are hidden by CSS, and then applying the CSSOM to calculate styles. By following these steps, the render tree includes the nodes that should actually be displayed on the screen along with their calculated styles.

When a render tree is created from the DOM and CSSOM used as examples in DOM Construction and CSSOM Construction, it looks like this:

Render tree

Among the nodes that existed in the DOM, p > span is not included in the render tree because display: none is set.

4. Layout

Layout is the step that determines the position and size of the nodes included in the render tree. While the render tree contains information about nodes to be displayed and their styles, calculations for their position and size within the device's viewport have not yet been performed, so they are handled in this step. Additionally, this step is called layout the first time it occurs, and may be referred to as reflow from the second time onwards.

Layout calculates the position and size of each node within the viewport from the render tree and converts it into a box tree. This process traverses from the root of the render tree (body) and calculates the position and size of each node relative to the viewport. For elements like images that have not yet finished loading, a placeholder space is created, and a reflow occurs once they are loaded.

The more nodes there are to be laid out, the greater the amount of calculation, resulting in longer processing times. Even small updates can have a large scope of impact, so layout processing is often relatively resource-intensive.

5. Paint + Composite

Paint and composite are the steps that display pixels on the screen. Since the previous processes have already determined what needs to be drawn, those results are used to display pixels on the screen. Because the box tree does not provide the order in which elements should be drawn, the drawing order is calculated before the actual drawing takes place.

Data required for drawing is created during the paint step, and the screen rendering is performed during the composite step.

This separation enables optimization; for example, during repaints triggered by certain animations or scrolling, the paint step is skipped and only the composite step is executed. While rendering processes up to the paint step are executed on the main thread, many tasks in the composite step are handled on a separate thread, meaning some animations and scrolling do not block the main thread.

Screen Updates

The screen displayed by the browser doesn't just show once and end; it is updated by various triggers. For example, when CSS properties are changed via JavaScript or CSS animations, the screen updates.

The entire Critical Rendering Path is not executed for every update; only the necessary steps are. Which steps are required depends on the type of change, and I will introduce some of those here.

All steps of the Critical Rendering Path are as follows:

Steps of the Critical Rendering Path

The step written as "Parse" here represents DOM construction via HTML parsing and CSSOM construction via CSS parsing, not changes to the existing DOM or CSSOM.

Executing everything from Parsing onwards

Steps of the Critical Rendering Path

A clear example of starting from parsing is the document.write() API. It's an API that writes strings directly to the document, but since it's deprecated, you likely won't see it much.

Other than APIs, for example, if you dynamically insert <link rel="stylesheet">, CSS parsing will be executed. Depending on the content of the CSS, all steps might be executed.

Executing everything from Style onwards

Steps of the Critical Rendering Path

This occurs when you use JavaScript or CSS to change properties like left or width that affect the layout. These layout properties are resource-intensive because they require redrawing all elements affected by the change.

Style → Paint → Composite

Steps of the Critical Rendering Path

This happens when you change properties that only affect appearance, such as color or box-shadow, via JavaScript or CSS. These paint-only properties update visual elements without changing the layout, so the layout step is unnecessary, making the process relatively low-load.

(Style →) Composite

Steps of the Critical Rendering Path

This occurs when you change properties like transform or opacity via JavaScript, which require neither layout nor paint. These properties can be processed using only the style and composite steps, skipping the paint step as well.

If these properties are changed via CSS animations or during scrolling, the style step might not even be executed. Since neither requires changes to the DOM or CSSOM, it is possible for processing to occur solely in the composite step. As mentioned above, many tasks in the composite step are handled on a separate thread, which has the advantage of not blocking the main thread.

Conclusion

I have summarized the mechanism by which browsers display web pages.

I think being aware of the overview of each step in the Critical Rendering Path and which steps are executed when the screen is updated will be helpful for performance optimization.

Regarding the initial load, I feel that resource loading executed during HTML parsing likely has a significant impact on optimization. Since network I/O is incredibly slow compared to internal browser processing, it might be best to focus on optimizing that first.

In real browsers, many optimizations not mentioned here are often performed, so it's impossible to understand everything from this post alone. However, I believe the basic flow of the Critical Rendering Path remains largely the same. I hope the knowledge in this post helps you understand the information shown in the Performance tab of the browser DevTools.

References

Discussion