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:
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:
Parsing HTML into the DOM takes place as a Task on the Main Thread. It is presented visually in a trace like this:
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 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.
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:
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:
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;
}
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 asdisplay: 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:
- Querying the CSS selectors against the aggregated rules in the CSSOM for the
div
element to get the applicable rules - 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:
- Recursively traverse the DOM, searching for visual elements
- Construct / update the Render Tree node pointing back to the DOM node
- 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:
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:
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:
- The browser creates and assigns a box for the
body
element. Its width is the full width of the screen, because it's ablock
- To get the height, the browser traverses to the
body
element's children (the threediv
elements, alsoblock
boxes) - The height of each of these
div
block
s is derived from their child, theTextNode
- The heights are aggregated recursively up, and precise coordinates and heights are assigned
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:
In some cases, if you force a synchronous reflow, it's possible that you will see Layout appear within a JavaScript task:
Paint
Let's take a look at our overall process flowchart (we're almost there!):
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:
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.