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-Typeheaders 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:
DOCTYPEStartTag(e.g.,<div>)EndTag(e.g.,</div>)CommentCharacter(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:
- When a
StartTagtoken (like<body>) arrives, the tree builder delegates the actual creation of the node to a helper class called theHTMLConstructionSite. - The
HTMLConstructionSiteinstantiates the corresponding C++ DOM object (in this case, anHTMLBodyElement), attaches it to the parent node in theDocument, and pushes the new element onto the Stack of Open Elements. - If
Charactertokens arrive next, they are grouped into aTextnode and appended as a child to the element currently at the top of the stack. - When a matching
EndTagtoken (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.