How Chromium's Blink Engine Parses HTML into a DOM Tree

This article explains the exact process Chromium’s Blink rendering engine uses to transform raw HTML bytes received from the network into a fully functional Document Object Model (DOM) tree. By passing data through a multi-stage pipeline—consisting of byte decoding, tokenization, and tree construction—Blink efficiently translates binary data into a hierarchical structure of C++ objects that the browser can ultimately style, layout, and paint onto the screen.

1. Decoding Bytes into Characters

The parsing pipeline begins the moment Chromium’s network stack delivers chunks of raw HTML bytes to Blink’s main thread. The HTMLDocumentParser is instantiated to manage the process.

Before Blink can understand the HTML, it must convert the raw bytes into actual characters. A component called the TextResourceDecoder inspects the incoming byte stream to determine the file's character encoding (such as UTF-8 or ISO-8859-1). It determines this by checking:

  • HTTP Content-Type headers sent by the server.
  • A Byte Order Mark (BOM) at the very beginning of the file.
  • <meta charset="..."> tags within the first 1024 bytes of the HTML.

Once the encoding is locked in, the decoder translates the raw bytes into a stream of Unicode characters (code points). Blink stores these characters in a specialized data structure called a SegmentedString, which acts as the input stream for the next stage.

2. Tokenization: Characters to Tokens

The SegmentedString feeds characters one by one into the HTMLTokenizer. The tokenizer's job is to break the continuous stream of characters into meaningful chunks called tokens. In Blink, these are represented by the HTMLToken class, which can take one of several types:

  • DOCTYPE
  • StartTag (e.g., <div>)
  • EndTag (e.g., </div>)
  • Comment
  • Character (text content inside tags)
  • EndOfFile

The HTMLTokenizer operates as a strict state machine defined by the WHATWG HTML5 specification. It contains over 60 states.

For example, the tokenizer begins in the Data State. When it encounters the < character, it switches to the Tag Open State. As it reads the next character, such as d, it transitions to the Tag Name State and begins accumulating characters (d, i, v) into a StartTag token until it hits the > character. Once the > is read, the current token is emitted, and the tokenizer returns to the Data State to await the next character.

3. Tree Construction: Tokens to DOM Nodes

As soon as the HTMLTokenizer emits a token, it is immediately consumed by the HTMLTreeBuilder. This component is responsible for taking the flat list of tokens and organizing them into a nested tree structure.

Like the tokenizer, the HTMLTreeBuilder is a state machine. Its states are called "Insertion Modes" (such as InitialMode, BeforeHtmlMode, InHeadMode, and InBodyMode). The current Insertion Mode dictates how an incoming token is processed.

To keep track of the hierarchy, the tree builder maintains a Stack of Open Elements. The process works as follows:

  1. When a StartTag token (like <body>) arrives, the tree builder delegates the actual creation of the node to a helper class called the HTMLConstructionSite.
  2. The HTMLConstructionSite instantiates the corresponding C++ DOM object (in this case, an HTMLBodyElement), attaches it to the parent node in the Document, and pushes the new element onto the Stack of Open Elements.
  3. If Character tokens arrive next, they are grouped into a Text node and appended as a child to the element currently at the top of the stack.
  4. When a matching EndTag token (like </body>) arrives, the tree builder pops that element off the Stack of Open Elements, signaling that no more children can be added to it.

The HTMLTreeBuilder also contains complex error-handling logic. Because the HTML5 specification dictates precisely how browsers must handle malformed HTML, Blink uses the Stack of Open Elements to automatically close unclosed tags or rearrange improperly nested elements (like a <div> inside a <p>) on the fly.

4. Handling Blocking Scripts and the Preload Scanner

The DOM construction process runs synchronously on the main thread, meaning it can be paused. When the HTMLTreeBuilder encounters a standard <script> tag, it must halt DOM construction entirely and execute the JavaScript. This is necessary because the script might contain document.write(), which injects new HTML directly into the character stream, or it might query the DOM as it currently exists.

Waiting for external scripts to download and execute would severely slow down page loads if the parser simply sat idle. To solve this, Blink uses an HTMLPreloadScanner.

When the main parser is blocked by a script, the HTMLPreloadScanner spins up. It takes a snapshot of the remaining unparsed HTML and runs a lightweight, speculative tokenization pass over it. It does not build the DOM; instead, it looks exclusively for tags that reference external resources, such as <link>, <script>, and <img>. When it finds them, it instructs Chromium's network stack to start downloading those files in the background. By the time the blocking script finishes executing and the main HTMLTreeBuilder resumes, the upcoming resources are already downloaded or in flight.

5. Parsing Completion

Once the HTMLTokenizer reaches the end of the SegmentedString and emits the EndOfFile token, the HTMLTreeBuilder pops any remaining elements off the Stack of Open Elements. At this point, the Document readiness state changes to interactive, the DOMContentLoaded event fires, and the fully realized C++ DOM tree is ready to be combined with the CSS Object Model (CSSOM) to build the layout tree for rendering.