An Introduction to the Browser Rendering Pipeline<!-- --> | <!-- -->Web Performance Tips

An Introduction to the Browser Rendering Pipeline

Users love pixels (frames) delivered on screen as fast as possible; that's what makes a web application feel fast!

HTML and CSS are the foundational building blocks web developers use to build visual experiences on the web. But how does the browser convert HTML and CSS into pixels?

The browser orchestrated transformation process of HTML and CSS into pixels is fairly opaque, and as a result, most web developers don't consider how or when it runs.

Understanding this process is key to building high performance web applications, and in this tip, I'll help demystify it.

The Overall Process

The journey from raw HTML and CSS text all the way to visual on-screen pixels has many steps and intermediate states along the way. For this beginner-friendly tip, we'll discuss the following:

A diagram showing HTML Text input to the DOM Tree, CSS Text becoming the CSSOM. CSSOM and DOM become the Styled Render Tree and the styled Render tree gets positioned via Layout. Finally, Pixels are drawn via Paint

HTML Parsing

When a user navigates to a web page, the entry to the application is the HTML document served to the user as a text file.

The browser utilizes its HTML Parser to convert this HTML text into the Document Object Model, or DOM.

The DOM is a tree that defines the structure of the document. Each node in the DOM tree is a browser object that maps back to items specified in the HTML text file.

For example, consider the following HTML text:

<!DOCTYPE html>
<html>

<head>
    <title>Example Document</title>
    <link rel="stylesheet" href="styles.css" />
</head>

<body>
    <div>Example Div 1</div>
    <div>Example Div 2</div>
    <div>Example Div 3</div>
</body>
</html>

Once the HTML parser finishes its job, the following DOM tree would be produced:

The produced DOM Tree from the HTML text above.

Parsing HTML into the DOM takes place as a Task on the Main Thread. It is presented visually in a trace like this:

The DOMParser running as a Task on the Main Thread in the Profiler.

CSS Object Model

While HTML defines the structure of the document, it may also reference CSS Stylesheets to define the visual presentation of the document.

These stylesheets are are typically defined via <link rel="stylesheet" /> tags, but may also be defined through inline <style> nodes.

A CSS stylesheet defines rules composed of selectors and declarations. For example, consider styles.css:

div {
    background-color: red;
}

In this example, div is a selector and background-color: red is a declaration. The entire block is considered a rule:

A diagram highlighting the differences between a rule, selector, and declaration.

A web application may reference many stylesheets, and a stylesheet may define many rules (often with many declarations!).

The browser will parse these stylesheets into a memory-efficient, lookup-efficient data structure, called the CSS Object Model, or CSSOM. Its primary purpose is to aggregate rules and provide an efficient lookup to match selectors to their declared styles.

A diagram showing CSS transforming from Text to CSSOM Aggregation

Downloaded text stylesheets get parsed and aggregated into the CSSOM on the Main Thread within a Task. This will manifest as a Parse Stylesheet Task in a trace:

A screenshot of the Chromium F12 Profiler showing a Parse Stylesheet task

Note: The CSSOM's exact backing structure is browser-specific and worth a dedicated article on its own. While I won't cover it in this tip, you can imagine it's a memory-efficient tree optimized for fast lookup. You can explore the StyleEngine source code for in-depth details.

Style and The Render Tree

Let's take a moment to examine the overall process flowchart:

A diagram showing the overall browser rendering phases, with Style phase circled.

Once the DOM and CSSOM are constructed, the browser can begin the next phase of the pipeline: Style. This phase is sometimes called Recalculate Style or a Render Tree Update.

The Render Tree

The Render Tree (sometimes called the Layout Tree) is a browser-internal C++ data structure that web developers don't directly modify.

It is a separate tree from the DOM but often mirrors its structure. Each node typically references a DOM node and a Computed Style. It's essentially composed of every DOM node that should be presented visually on the user's screen.

Consider this simplified Render Tree node, called a RenderObject:

class RenderObject {
    private:
        // Reference to the DOM Node being represented
        DOMNode* domNode;

        // Reference to the CSS Styles resolved for this DOM node.
        ComputedStyle* style
        
        // RenderTree structural pointers
        RenderObject* parent;
        RenderObjectList* children;
}

A diagram showing a RenderObject referencing a DOMNode and a ComputedStyle

The Render Tree is also responsible for other, non-DOM related visual elements, such as scroll bars and text selection.

Consider reading through Chromium's LayoutTree implementation for more in-depth details on this data structure.

Note: Notable exclusions from the Render Tree include <head> and its children, anything that is marked as display: none, and <script> elements because they are not presented to the user's screen.

ComputedStyle

A ComputedStyle is effectively the list of CSS declarations that apply to that DOM node, considering the DOM node's selector, CSS specificity, and the aggregated rules in the CSSOM.

For example, if I have an example HTML Element:

<div class="center-text">Example</div>

and styles defined like this:

.center-text {
    text-align: center;
}

div {
    background-color: red;
}

The ComputedStyle for my element would be constructed via:

  1. Querying the CSS selectors against the aggregated rules in the CSSOM for the div element to get the applicable rules
  2. Resolving any CSS specificity conflicts to the final set of declarations applied, in this case:
    • text-align: center
    • background-color: red
    • any of the default styles defined by the browser

Consider reading through Chromium's ComputedStyle implementation and StyleResolver for more in-depth details on this process.

Tree Construction

To build the Render Tree, the browser will:

  1. Recursively traverse the DOM, searching for visual elements
  2. Construct / update the Render Tree node pointing back to the DOM node
  3. Derive ComputedStyles for that DOM node, and associate with the DOM node and Render Tree node

In the end, we end up with a styled Render Tree of visual elements to present the user:

A diagram showing the Render Tree referencing back its DOM Nodes

If this diagram is too small to read (I tried to include everything 😅), try to right-click and open in new tab.

In the Chromium Profiler, this will appear as a Recalculate Style task:

A screenshot of the Chromium Profiler referencing Recalculate Style

Layout

Although the Render Tree contains all the CSS declarations for widths, heights, colors, etc. for each visual element on the page, the browser has not assigned any geometry or coordinates to the elements.

The Layout process (sometimes called Reflow) recursively traverses the newly constructed / updated Render Tree, and assigns each node precise floating-point positions and geometry.

Layout is a very deep and complex topic. For the purposes of this tip, what's important to know is that Layout will create and position boxes for each node in the Render Tree.

For example, in our example Render Tree:

  1. The browser creates and assigns a box for the body element. Its width is the full width of the screen, because it's a block
  2. To get the height, the browser traverses to the body element's children (the three div elements, also block boxes)
  3. The height of each of these div blocks is derived from their child, the TextNode
  4. The heights are aggregated recursively up, and precise coordinates and heights are assigned

A screenshot of the body element's height.

A screenshot of the div element's height.

This very cool video shows the browser assigning geometry recursively through the Layout process:

One thing to note here is that the Layout process can be quite expensive, so the browser uses extensive caching to avoid re-computing Layout unnecessarily.

Layout will typically appear in the Profiler during the Rendering phase, like this:

A screenshot of the Chromium Profiler highlighting Layout.

In some cases, if you force a synchronous reflow, it's possible that you will see Layout appear within a JavaScript task:

A screenshot of the Chromium Profiler highlighting a forced reflow.

Paint

Let's take a look at our overall process flowchart (we're almost there!):

The overall browser rendering process, with Paint highlighted.

Once we have a styled, positioned set of Render Tree nodes, the browser utilizes a computational graphics library to draw the Render Tree nodes programmatically as pixels.

This process is quite nuanced, but, from a high level, the browser traverses the newly positioned Render Tree recursively, and executes instructions to draw each Render Tree node.

This phases is responsible for making sure each visual element is painted in the correct order (i.e., resolving z-index, scroll containers, etc.).

Chromium utilizes the Skia library to facilitate drawing, and Skia will interface with the GPU for lower-level OpenGL / DirectX graphics instructions.

Once textures are produced from the GPU, the browser aggregates them into a Frame, and the Frame is submitted to the user's display!

Paint is unique in that the work spans multiple threads and processes to complete, but in general it'll manifest in the Chromium Profiler like this:

A screenshot of the Chromium Profiler highlighting Paint.

Note: I am intentionally leaving off the advanced details of Layers and Compositing. Consider reading more about those in my Layers and Compositing tip.

When does Rendering Run?

We've described how the browser renders, but when does it run?

For this answer, read my tip on the browser event loop.

Conclusion

As web developers, it's important to understand that our favorite CSS, HTML, React components, etc. cannot present themselves visually to the user until the browser completes these phases.

Consider how to measure frame paint time next, or go deeper into Browser Rendering with Layers and Compositing.

That's all for this tip! Thanks for reading! Discover more similar tips matching Browser Internals.