On this page
Why Is the Web Still Built on Text?
Why the web still relies on text, from CPU cache physics to the V8 JIT pipeline, LayoutNG fragment trees, and WebAssembly security boundaries.
The web platform is one of the most heavily optimized distributed systems ever built.
Modern browser engines comprise tens of millions of lines of C++ code, capable of compiling WebAssembly, rendering hardware-accelerated WebGPU pipelines, and JIT-compiling JavaScript into highly optimized machine code.
Yet, despite this immense computational power, the foundational layer of the web (the Document Object Model, or DOM) is still instantiated entirely from a serialized string of character data transmitted over HTTP.
For developers accustomed to zero-copy binary serialization formats, this text-centric architecture can seem like a legacy bottleneck.
If we are shipping complex 3D graphics and heavy animations, why do we still transmit the presentation layer as plain text?
The short answer: HTML did not survive because the web failed to invent a binary replacement. It survived because text provides a deeply resilient, compressible, and interoperable intermediate representation.
Text acts as the ultimate memory-safe boundary between the network, the C++ layout engine, the JavaScript virtual machine, OS-level accessibility APIs, and autonomous AI agents.
This article traces the lifecycle of that text representation from network packets to immutable layout fragments, detailing the exact engine-level mechanics that make a text-based web viable.
The Representation Gap: Why Text Won
To understand why the web is built on text, we must examine the alternatives that failed. In the late 1990s, the limitations of early HTML drove companies to create proprietary alternatives.
Technologies like Macromedia Flash, Java Applets, and Microsoft Silverlight attempted to replace HTML with opaque, compiled binary blobs.
They promised perfect pixel-level control and zero-copy binary execution, completely bypassing the browser’s rudimentary layout engine to operate as isolated black boxes within the window.
The architectural failure of these plugins was a fundamental failure of the DOM boundary. Because they operated as binary blobs, they could not participate in the browser’s native lifecycle.
They broke standard HTTP caching immediately. They thwarted search indexers completely, because Googlebot parses HTML, not compiled bytecode.
Screen readers could not pierce the binary canvas, destroying native accessibility integrations while serving as the primary attack vector for decades of malware.
HTML succeeded because its textual nature forced a strict decoupling between the data representation and the presentation engine.
By ensuring the payload was declarative text, the web platform guaranteed that the browser retained ultimate authority over how that data was rendered, scaled, cached, and secured.
Text is the ultimate open standard.
The Physics of the Network: HTTP/3, QPACK, and BBR
When a browser requests an HTML document, the response faces the physics of the transport layer. Under TCP, connections undergo “TCP Slow Start.”
The server initially sends a small congestion window of packets (often around 14KB, or exactly 10 TCP segments of 1460 bytes each). The server gradually increases the flow based on acknowledgment packets (ACKs) from the client.
Because HTML is a sequential text format, it natively supports chunked transfer encoding, allowing the browser’s parser to speculatively execute and render that initial 14KB chunk instantly without waiting for the rest of the stream.
Modern transport layers amplify this advantage dramatically. Google’s BBR congestion control algorithm measures the exact bottleneck bandwidth and paces packet delivery accordingly.
A text-based HTML stream feeds perfectly into this paced pipeline. As BBR delivers packets precisely at the optimal rate, the HTML parser consumes them at an identical rate.
With modern HTTP/3 over QUIC, this advantage multiplies. QUIC multiplexes independent byte streams over UDP, entirely eliminating TCP head-of-line blocking.
The text payload is compressed using QPACK (the HTTP/3 header compression standard) and encapsulated inside a QUIC STREAM frame.
// Conceptual HTTP/3 QUIC STREAM Frame wrapping HTML Text
[Frame Type: 0x08 (STREAM)]
[Stream ID: 0x04]
[Offset: 0x00]
[Length: 0x2A (42 bytes)]
[Payload Data (UTF-8 bytes of HTML)]:
3c 64 69 76 20 63 6c 61 73 73 3d 22 68 65 72 6f 22 3e
< d i v c l a s s = " h e r o " >
Highly structured binary formats are rigidly inflexible here. They typically require full schema resolution, complete block boundaries, and offset tables located at the absolute end of the file.
This makes progressive rendering across multiplexed streams nearly impossible. Text allows the parser to chew through bytes the exact millisecond they arrive on the network interface.
CPU Cache Physics: Pointer Chasing vs Sequential Text
There is a profound hardware-level reason why text parsing is so fast: CPU cache physics.
Modern CPUs operate at blistering speeds, but reading from main RAM is incredibly slow. A “cache miss” can cost hundreds of CPU cycles. To avoid this penalty, CPUs pull memory into L1 and L2 caches in contiguous 64-byte chunks called cache lines.
Binary formats that rely on complex memory graphs are dangerous at this layer. They require traversing pointers to random locations in memory, a process known as pointer chasing.
Pointer chasing is a disaster for modern CPUs because the hardware prefetcher cannot predict which memory address will be read next, resulting in continuous L1 cache misses that stall the CPU pipeline entirely.
A raw HTML string is a contiguous array of bytes. As the HTML parser scans the text, the CPU’s hardware prefetcher can perfectly predict the sequential access pattern.
It pulls the text stream into the L1 cache flawlessly, easily offsetting the apparent slowness of text parsing through the sheer mechanical efficiency of sequential memory access on modern silicon.
The Parsing Pipeline: HTMLDocumentParser and the Spec
Transforming a UTF-8 byte stream into a C++ DOM tree requires a highly specific pipeline explicitly defined in the WHATWG HTML Standard.
In Blink (Chromium), this pipeline lives primarily in third_party/blink/renderer/core/html/parser/ and is heavily multi-threaded to prevent main-thread jank.
The bytes arrive on the network thread and are immediately dispatched to the BackgroundHTMLParser.
- Decoding: The background thread handles character decoding. If the document is UTF-8, it converts the bytes into UTF-16
WTF::Stringbuffers. This ensures all subsequent string operations use a consistent 16-bit memory layout. - Tokenization: The
HTMLTokenizeracts as a strict state machine. It transitions through 80 normative states (e.g.,Data state,Tag open state) and chops the character stream into lexicalHTMLTokeninstances. - Cross-Thread IPC: The background thread batches these tokens and sends them to the main thread via cross-thread message passing.
- Tree Construction: On the main thread, the
HTMLTreeBuilder::ProcessTokenloop runs to instantiate the actual C++ objects (likeblink::HTMLParagraphElement).
The HTMLTreeBuilder itself is a massive state machine controlled by Insertion Modes (like in head or in body). Depending on the mode, a token might be processed entirely differently.
If a text token arrives during the in head mode, the parser automatically closes the <head> tag, assuming the developer simply forgot to write </head>.
// third_party/blink/renderer/core/html/parser/html_document_parser.cc
void HTMLDocumentParser::PumpTokenizer() {
while (tokenizer_->NextToken(input_.Current(), token_)) {
// Process the text token into a C++ DOM Node
tree_builder_->ProcessToken(token_);
// Check if we've blocked the main thread for too long (>50ms)
if (yield_timer_.ShouldYield(kMaxParseTime)) {
// Yield to the V8 event loop so the browser can paint or handle input
yield_timer_.YieldAndPostTask(
WTF::BindOnce(&HTMLDocumentParser::PumpTokenizer,
WrapPersistent(this)));
return;
}
}
}
Notice the yield_timer_ in the code above.
If parsing a massive HTML payload threatens to block the main thread, the parser yields control back to the V8 event loop after roughly 50ms of continuous execution.
This allows the browser to perform a Microtask Checkpoint, resolving Promises, running MutationObserver callbacks, and painting frames before resuming tokenization precisely where it left off.
Blocking vs Non-Blocking: The Preload Scanner Priority Queue
HTML parsing is deeply intertwined with network fetching. When the HTMLTreeBuilder encounters a synchronous <script> tag, it must halt tree construction, fetch the script, execute it in V8, and only then resume parsing.
This safety mechanism exists because the script could theoretically call document.write(), fundamentally mutating the text stream ahead of the parser.
To prevent this from stalling the entire page load, Blink spins up an HTML Preload Scanner that runs independently on the background thread.
The Preload Scanner bypasses the main HTMLTreeBuilder entirely, using a simplified heuristic tokenizer to scan ahead in the raw text stream.
It hunts for src and href attributes on <script>, <link rel="stylesheet">, and <img> tags.
When it finds them, it immediately pushes them into a network priority queue. CSS files are dispatched with Highest priority because CSS blocks the render tree. Synchronous JavaScript is dispatched with High priority.
Images in the viewport receive High priority, while out-of-viewport images receive Low priority. By eagerly dispatching network requests before the main parser reaches their tags, text enables massive network parallelization.
String Interning: The Secret to Text Performance
A common objection to text is memory bloat. If a document contains ten thousand <div class="btn"> elements, does the browser allocate the string "btn" ten thousand times?
No.
Browser engines use a highly optimized technique called String Interning, handled in Blink by the AtomicString class.
When the parser encounters an attribute or tag name, it hashes the string and checks a global hash table. If the string "btn" already exists, it does not allocate new memory; it simply returns a pointer to the existing string.
// How Blink uses AtomicString to save memory and CPU cycles
WTF::AtomicString class_name = AtomicString("btn");
WTF::AtomicString other_class = AtomicString("btn");
// Because both strings point to the same address in the intern table,
// string comparison becomes a lightning-fast pointer equality check:
if (class_name.Impl() == other_class.Impl()) {
// True. Evaluated in a single CPU instruction.
}
This means the memory footprint of textual redundancy collapses to near-zero in RAM. It also revolutionizes the speed of the CSS engine.
When the CSS engine matches a selector against the DOM, it avoids slow character-by-character string comparisons and simply compares the memory pointer addresses.
This is why text-based DOM manipulation remains incredibly fast despite the verbosity of HTML.
CSS Bloom Filters: Rejecting Text Faster
Even with pointer equality, matching CSS rules against thousands of DOM nodes is mathematically expensive.
Blink optimizes this text-based matching process using an algorithmic shortcut called a Bloom Filter.
This is a probabilistic data structure used to quickly test whether an element is a member of a set.
When Blink parses a stylesheet, it hashes all the CSS selectors and adds them to the Bloom Filter. When it walks the DOM tree, it hashes the element’s classes and IDs and checks the Bloom Filter immediately.
If the Bloom Filter says “No,” Blink guarantees the CSS rule does not apply, skipping the expensive pointer matching entirely.
If the Bloom Filter says “Yes,” Blink performs the actual check. This algorithmic shortcut skips millions of unnecessary text comparisons during a complex page layout.
The DOM Memory Layout: What is a Node?
When the text is parsed, it becomes a heavily structured C++ object.
At the top of the inheritance chain is blink::EventTarget, followed by blink::Node, blink::ContainerNode, and blink::Element, eventually reaching specific tags like blink::HTMLElement.
// Simplified memory layout of a Blink C++ DOM Node
class Element : public ContainerNode {
// A pointer to a separate object holding attributes (id, class, style)
// This is lazy-loaded to save memory on empty tags!
Member<ElementData> element_data_;
};
class Node : public EventTarget {
// Pointers for tree traversal
Member<Node> parent_or_shadow_host_node_;
Member<Node> previous_;
Member<Node> next_;
// A bitfield containing 32 boolean flags for tree state
uint32_t node_flags_;
};
An empty <div> in memory is not just a few bytes; it is a complex web of pointers linking to its parent, siblings, and children.
It points to its lazily-allocated ElementData which holds its classes and attributes.
This C++ graph is immensely powerful, but it requires serious garbage collection to prevent memory leaks when nodes are detached from the tree.
The JavaScript Boundary: Oilpan, Orinoco, and ScriptWrappable
JavaScript cannot directly read C++ memory, meaning the text-to-DOM pipeline must cross a critical boundary into the JavaScript virtual machine (V8). For every C++ DOM node that a script touches, Blink generates a V8 Wrapper Object.
All DOM nodes in Blink inherit from ScriptWrappable, meaning when JS calls document.querySelector('p'), Blink allocates a JavaScript object inside V8’s heap and links it to the underlying C++ object.
V8 uses highly optimized memory representations for text. If a text node contains only ASCII characters, V8 allocates a SeqOneByteString. If it contains complex Unicode, it upgrades to a SeqTwoByteString.
If you concatenate two strings in JS, V8 does not defensively copy the bytes; it creates a ConsString, a binary tree pointing to the two original strings.
// Conceptual view of how Blink maps a C++ Element to V8 Memory
class HTMLParagraphElement : public HTMLElement, public ScriptWrappable {
public:
// GC tracing for Blink's C++ Garbage Collector (Oilpan)
void Trace(Visitor* visitor) const override {
// Tell the GC not to destroy the JS wrapper if this C++ node is alive
visitor->Trace(wrapper_);
HTMLElement::Trace(visitor);
}
private:
// Cross-component tracing pointer to V8's Orinoco Garbage Collector
TraceWrapperV8Reference<v8::Object> wrapper_;
};
This boundary introduces immense garbage collection complexity. The C++ node is managed by Blink’s GC (Oilpan), while the JS wrapper is managed by V8’s GC (Orinoco). Oilpan uses a Mark-Sweep-Compact algorithm.
If a script holds a reference to a detached DOM node, Orinoco traces the TraceWrapperV8Reference and communicates with Oilpan to ensure the underlying C++ Node isn’t prematurely destroyed.
The V8 Pipeline: Text to Ignition Bytecode to TurboFan
JavaScript itself is just text. When the HTML parser encounters a <script> tag, it halts and hands the text over to V8.
The V8 engine does not execute text directly. It pushes the text through an incredibly deep compilation pipeline:
- The Scanner: V8 scans the text. It converts it into a stream of tokens.
- The Parser: V8 parses the tokens into an Abstract Syntax Tree (AST).
- Ignition Interpreter: The Ignition interpreter walks the AST. It converts it into V8 Bytecode. This bytecode executes immediately, allowing the page to load fast.
- TurboFan JIT Compiler: As the bytecode runs, V8 profiles it. It notes that a variable
xis always an integer, for example. If a function becomes “hot” (called frequently), it is passed to TurboFan. - Machine Code: TurboFan optimizes the bytecode. It compiles it into highly optimized, native machine code for your specific CPU architecture (x64 or ARM64).
If the assumptions TurboFan made turn out to be false (e.g., you pass a string to a function that previously only saw integers), TurboFan performs a Deoptimization. It executes a “bailout”.
It throws away the machine code and falls back to the Ignition interpreter text-bytecode safely.
Fault Tolerance: The Adoption Agency and Foster Parenting
Strict binary serialization formats are highly brittle, often throwing fatal errors if a single byte is corrupted. The HTML Standard specifies deterministic error-recovery behavior instead.
If an author writes overlapping markup, the parser invokes the notorious Adoption Agency Algorithm, which is an 8-step C++ mutation process designed to prevent tree corruption.
Take the overlapping string: <b><p>Bold text</b></p>
[Stack of open elements when the parser hits '</b>']
1. html
2. body
3. b (The Formatting Element)
4. p (The Furthest Block)
[Adoption Agency Algorithm - Reparenting Phase]
1. Remove children of <b> (the text node "Bold text")
2. Append them to <p>
3. Clone <b> -> <b>'
4. Insert <b>' inside <p>
The engine dynamically transforms the malformed string into a memory-safe graph, seamlessly outputting <b><p>Bold text</p></b><p></p>.
Even more complex is Foster Parenting.
If a developer accidentally places a <div> inside a <table> but outside a <td>, the HTML specification dictates that the <div> must be “foster parented”, ejecting it to live just before the <table> element so the table structure doesn’t collapse.
The Compression Reality: Brotli’s LZ77 and Static Dictionaries
Writing out <div class="table-cell"> repeatedly seems wasteful compared to a packed binary struct, but modern HTTP compression algorithms are designed specifically to exploit the low entropy of UI markup.
Brotli achieves massive compression ratios through three specific algorithmic mechanisms.
First, it uses LZ77 Back-references with a sliding window up to 16MB. If it sees a repeated <div> tag, it doesn’t encode the string again, simply encoding a pointer tuple showing distance and length.
Second, it uses Dynamic Huffman Coding to build custom frequency tables on the fly, assigning the shortest bit sequences to the most commonly used characters in the text.
Third, it uses a Static Dictionary. Brotli ships with a pre-defined 120KB static dictionary containing 13,000 common web strings, entirely eliminating the cold-start compression penalty.
// Conceptual visualization of Brotli LZ77 over HTML
Uncompressed HTML:
<div class="card"><p>A</p></div><div class="card"><p>B</p></div>
Brotli Stream:
[Literal]: "<div class="card"><p>A</p></div>"
[Pointer]: Distance=32 bytes back, Length=26 bytes
[Literal]: "B</p></div>"
Zero-copy binary architectures require fixed memory layouts, which are hostile to runtime mutation. UI documents are highly variable; nodes are inserted, text is reflowed, and layouts recalculate constantly.
Text compression effectively eliminates structural bloat without sacrificing the extreme mutability of the DOM.
Beyond the DOM: LayoutNG, HarfBuzz, and Immutable Fragment Trees
The DOM is merely an intermediate representation; the engine must still compute the visual geometry. In Chromium’s modern layout engine (LayoutNG), this involves compiling the DOM and CSSOM into an immutable Fragment Tree.
The engine walks the DOM tree and computes final styles, generating a Layout Tree composed of LayoutObject instances (where a display: none text node exists in the DOM but safely disappears from the Layout Tree).
To render the actual text, the layout engine relies on an open-source text shaping engine called HarfBuzz.
HarfBuzz takes the Unicode text strings and the loaded font files, manages an hb_buffer_t struct, and mathematically calculates exactly which glyphs to draw and how to position them.
// A standard DOM fragment:
<p>Hello <strong>world</strong></p>
// The resulting Blink Layout Tree Output (LayoutNG)
LayoutBlockFlow {HTML} at (0,0) size 800x600
LayoutBlockFlow {BODY} at (8,8) size 784x584
LayoutBlockFlow {P} at (0,0) size 784x18
LayoutText {#text} at (0,0) size 36x17 "Hello "
LayoutInline {STRONG} at (0,0) size 38x17
LayoutText {#text} at (36,0) size 38x17 "world"
LayoutNG walks the LayoutObject tree to generate immutable NGPhysicalBoxFragment objects.
Because these fragments are strictly immutable, Blink can aggressively cache them and safely perform layout calculations concurrently across multiple threads.
Compositing: Text to cc::Layer, SkPicture, and GPU Glyph Caches
The final step in the pipeline bridges the gap between text layout and GPU rendering.
Once the NGPhysicalBoxFragment tree is complete, the Blink engine runs a Paint process, recording drawing primitives (like DrawRect and DrawTextBlob) into an SkPicture data structure used by the Skia graphics library.
To ensure silky smooth 60fps scrolling, Blink assigns certain subtrees of the layout to their own compositing layers (cc::Layer). When text is drawn, HarfBuzz has already determined the glyphs.
The engine rasterizes those vector glyphs and stores them in a GPU Glyph Cache, a massive texture atlas residing purely in VRAM.
When scrolling down a page, the GPU does not redraw the vector text; it simply splats the cached bitmap glyphs onto the screen in hardware.
The SkPicture for each layer is rasterized on worker threads, uploaded to GPU memory as textures, and handed to the Chrome Compositor (cc) to orchestrate their final position on the screen.
Semantic Structure: AXObjectCacheImpl and IPC
When Blink constructs the document, it simultaneously updates an internal Accessibility Tree.
This tree maps directly to native COM APIs, exposing the document structure to IAccessible2 or UIAutomation on Windows, NSAccessibility on macOS, and AT-SPI2 on Linux.
In Blink, this mapping is managed by the AXObjectCacheImpl class, which listens to DOM mutations and maps Element subclasses to AXNode elements.
The renderer process then serializes this tree over IPC (Inter-Process Communication) to the browser process, which communicates securely with the operating system’s screen readers.
This translation relies heavily on the Accessible Name and Description Computation, a recursive algorithm that traverses the textual DOM structure to extract ARIA attributes.
When text is dynamically injected via JavaScript, the engine triggers aria-live events, firing OS-level interrupts to announce the change immediately without requiring a full page refresh.
WebAssembly: Linear Memory, WebIDL, and Spectre Mitigations
WebAssembly (Wasm) brought a compiled, binary format to the browser, excelling at heavy compute tasks like cryptographic algorithms and video encoding.
So why doesn’t WebAssembly replace HTML?
Because WebAssembly operates in a tightly sandboxed linear memory space.
This memory isolation is critical for security following side-channel attacks like Spectre and Meltdown. Locking Wasm inside an isolated memory buffer prevents malicious binaries from reading the browser’s heap or accessing cross-origin data.
// Wasm can hold opaque references to JS objects, but cannot mutate them
// without bridging through the host environment (JavaScript bindings).
#[wasm_bindgen]
extern "C" {
pub type Element; // An opaque externref to the text-based DOM
#[wasm_bindgen(method, setter = innerHTML)]
pub fn set_inner_html(this: &Element, html: &str);
}
Because Wasm is locked away, it cannot directly manipulate the C++ DOM pointers. To touch the DOM, it must use WebIDL bindings to communicate across the JavaScript boundary.
Even with the new Wasm GC proposal and externref, it cannot bypass these bindings, meaning layout orchestration remains permanently tied to the text-originated DOM.
What AI changes about the web’s textual foundation
As AI agents generate interfaces, the text foundation proves its immense architectural value once again. Modern AI tools output HTML, CSS, and React components, providing the critical advantage that text is natively inspectable.
AI-generated HTML can be diffed, reviewed, version-controlled, searched, linted, parsed into an AST, and tested.
If an AI generates a complex interface, developers need a verifiable intermediate representation to audit the output before it reaches the user.
You cannot easily git diff an opaque binary canvas, nor can you easily inject a static analysis tool into a compiled WebGPU shader to check for accessibility violations.
Autonomous agents interacting with the web rely heavily on the accessibility tree. Scripts powered by GPT-4 use the raw HTML structure to understand spatial relationships on a page.
The web isn’t still built on text because the industry never found a replacement; it is built on text because text acts as the universal API between artificial intelligence, browser engines, and human UI.
From the team at
We build digital products and explore the modern web standards behind them.