Every modern browser performs the same core task: it takes a resource you request, fetches it over the network, and renders it on screen. The resource is usually an HTML document, but it can just as easily be a PDF, image, or other content type, identified by a URI. How the browser interprets and displays HTML is governed by the HTML and CSS specifications maintained by the W3C. For years, browsers only partially conformed to these standards and shipped proprietary extensions, causing serious compatibility issues for developers. Today, the major browsers are largely spec-compliant.
The presentation layer is where most of the complexity lives, but it's only one part of a larger system. Each browser is composed of several cooperating components:
User interface: the address bar, back/forward buttons, bookmarking menu, and everything else in the browser chrome except the page itself.
Browser engine: marshals actions between the UI and the rendering engine.
Rendering engine: parses HTML and CSS and paints the parsed content. Chrome, notably, runs one rendering engine instance per tab, each in its own process.
Networking: handles HTTP requests and other network calls, exposing a platform-independent interface over platform-specific implementations.
UI backend: draws core widgets such as combo boxes and windows through a generic interface that delegates to operating system UI methods underneath.
JavaScript interpreter: parses and executes JavaScript.
Data storage: a persistence layer that handles cookies and storage mechanisms such as localStorage, IndexedDB, WebSQL, and the FileSystem API.
Notably, the UI itself is not prescribed by any formal specification. Browser chrome conventions—like an address bar and refresh button—emerged from years of imitation and practice rather than standards. The HTML5 specification lists common elements such as the address bar, status bar, and toolbar, but mandates none of them. Features like Firefox's downloads manager are browser-specific additions.
The rendering engine
The rendering engine is responsible for turning a raw resource into an interactive page. It is the default engine for HTML and XML documents, and the parts discussed here concern that primary path. The same engine also powers PDF viewing in Chrome and Firefox via plugins.
Rendering engines differ in implementation, but the parsing flow is shared. The engine parses HTML into a DOM tree, then parses external CSS and style information to construct a render tree—a visual representation of the DOM where every node carries a computed style. Next, layout assigns each render tree node coordinates and dimensions on the viewport. Finally, the painting phase walks the render tree and draws each node using the UI backend layer.
In practice, rendering is not a linear process. A large page may spawn partial layouts and repaints. As the engine parses incrementally, it begins displaying content before the full document arrives, prioritizing perceived performance over strict completion of parsing and layout.
Parsing basics
Parsing is the first stage and the one that most shapes performance. It starts with a lexer that breaks the raw input into tokens, then a parser that applies grammar rules to build a parse tree from those tokens. The chapter is not a deep dive into parsing theory, but understanding the distinction matters because browsers take special care to ensure the parser doesn't block the critical rendering path.
Loading external resources is a key part of that. Each resource must be fetched over the network, and a slow resource can stall the parser. Browsers use a preloader to mitigate this: while the parser is busy constructing the DOM from HTML, the preloader scans ahead for tags like <img> and <link> and begins fetching them in parallel, so that when the parser catches up, the resources are already in progress or complete. This is a critical optimization, because the main parser is single-threaded and halts for network requests.
Only the original HTML parsing path through DOM construction is described here. Additional stages—like the rules for how CSS animations affect rendering, or how JavaScript execution interrupts layout—have their own complex rules.
The parser is not just a parser
Because HTML is a forgiving language, the browser's HTML parser does not match the strict error-handling model of a conventional parser. HTML5 formalizes the error handling rules that browsers use, and those rules are built directly into the parser's tokenizer state machine. This does not mean the parser discards well-known algorithms; it applies the official WHATWG HTML algorithms to faithfully reconstruct the DOM when fed malformed markup.
CSS parsing is likewise woven into the rendering flow. Parsed CSS is not simply transferred into the DOM. Instead, the browser converts it into a structure it can walk easily for style recalculation and matching. Selector matching—linking style rules to nodes—is a hot path in interactivity performance. More detail on those implementation choices is published in the CSS specifications.
JavaScript is handled in the same engine pipeline, not by the rendering engine's parser. The rendering engine hands script source to the JavaScript interpreter, which executes it. Script execution can influence the rendering path: scripts may modify the DOM, initiate fetches, or trigger layout when they access measured properties. Each of these interactions is a potential performance hazard and drives the engineering choices in mainstream browsers.
From DOM to screen
Once the DOM tree is ready, the render tree building and layout stages follow. In the render tree phase, the browser pairs DOM nodes that produce boxes with matched style rules. It computes the "used values" of all CSS properties for each node (e.g., resolving em to an absolute pixel value, and collapsing inheritance). The render tree only includes visible elements: <head> and anything with display: none are omitted.
Layout (also called reflow) is where the geometry is resolved. The root renderer boxes the viewport, and layout is performed recursively downwards through the tree. The output is a box tree with coordinates that the painter layer turns into actual pixels. Painting is done in layers to handle reordering, scrolling, and composited animations. If layout is the geometry, then paint determines visibility and z-order, producing a stacking context per positioned element or one with an explicit z-index.
Existing render tree nodes may need new layout after a change, e.g., an element's dimensions from content loaded later, or a script-triggered DOM mutation. The goal is to make such re-layout and repaint incremental and localized wherever possible. In practice, global relayouts are triggered for things like font loading or viewport resizing, both of which can invalidate the full frame tree.
Scripts and style sheets during parsing
The web platform treats scripts and style sheets as synchronous dependencies during parsing, but only if they are not explicitly marked otherwise.
A classic <script> tag blocks parsing. The document's parser halts; the browser fetches and executes the external JavaScript, then resumes. If the script parameter has the async attribute, the browser fetches the script in parallel while parsing, and the script is executed as soon as available—it can interrupt rendering. Scripts with the defer attribute are also fetched without blocking, but they wait until the document has finished parsing before executing, in order. Either attribute prevents parser blocking, but they have little effect on scripts that are inline, since the execution must happen immediately inside the markup.
Style sheets were historically not blocking for the parser, because CSS could be parsed in the background and did not risk altering the DOM. However, incorrect styling can lead to a flash of unstyled content (FOUC), and modern browsers parse CSS in a way that often blocks script execution. If a script tries to read computed geometry that depends on style, the browser may need to block that script until style resolution is complete.
Modern browser internals achieve interactive pages through careful ordering: fast HTML tokenization, incremental layout, preloading, and strategic blocking early on network and execution. The balance shifts with each major release, tuned against real-world pages and with the specs tightened around verifiable behavior.
Rendering Engines and the Main Flow
The rendering engine’s job is to display the requested content on the browser screen. By default, it handles HTML, XML documents, and images. Other data types (like PDFs) are handled via plug-ins or extensions. Our focus here is the primary case: HTML and images styled with CSS.
Rendering engines vary by browser. Internet Explorer uses Trident; Firefox uses Gecko; Safari uses WebKit. Chrome and Opera (from version 15) use Blink, a fork of WebKit. WebKit is an open-source engine originally built for Linux and later adapted by Apple for Mac and Windows.
The engine receives document contents from the networking layer, typically in 8kB chunks. From there, the core flow is consistent:
Figure 2: Rendering engine basic flow
The engine parses the HTML into a "content tree" of DOM nodes, then parses style data from external CSS files and inline style elements. Styling information plus visual instructions in the HTML produce a second tree: the render tree. This tree holds rectangles with visual attributes (color, dimensions) ordered for screen display.
After building the render tree, the engine runs a "layout" process, assigning each node exact screen coordinates. Then comes painting—the engine traverses the render tree and paints each node using the UI backend layer.
This process is gradual. For better user experience, the engine displays content as soon as possible, not waiting for the full HTML parse. It builds and lays out render tree parts incrementally while the rest of the content streams in from the network.
WebKit and Gecko use different terms but follow the same flow:
Figure 3: WebKit main flowFigure 4: Mozilla's Gecko rendering engine main flow
Gecko calls the tree of visually formatted elements a "Frame tree" (each element a frame); WebKit uses "Render Tree" with "Render Objects." WebKit says "layout" for placing elements; Gecko calls it "Reflow." WebKit's "Attachment" is the process linking DOM nodes and visual data into the render tree. A minor difference: Gecko has an extra "content sink" layer between HTML and the DOM tree, acting as a factory for DOM elements.
Parsing Fundamentals
Parsing is central to the rendering engine, so we’ll examine it in depth. It translates a document into a structure the code can use—usually a tree of nodes representing document structure, called a parse tree or syntax tree. For instance, parsing the expression 2 + 3 - 1 could yield:
Figure 5: mathematical expression tree node
Grammar
Parsing depends on the document’s syntax rules—its language or format. Every parseable format must have a deterministic grammar comprising vocabulary and syntax rules, known as a context-free grammar. Human languages aren’t context-free and thus can’t be parsed with conventional techniques.
Lexer and Parser
Parsing splits into two sub-processes: lexical analysis (breaking input into tokens—the language’s valid building blocks) and syntax analysis (applying language syntax rules).
Parsers divide work between a lexer (or tokenizer), which breaks input into valid tokens and strips irrelevant characters like whitespace and line breaks, and a parser, which builds the parse tree by matching tokens against syntax rules.
Figure 6: from source document to parse trees
Parsing is iterative. The parser requests a token from the lexer and tries matching it to a syntax rule. On a match, a node is added to the parse tree and the parser asks for the next token. If no rule matches, the parser stores the token internally and continues asking until a rule matches all stored tokens. If none ever match, it raises an exception—signaling invalid syntax.
Translation
Often the parse tree isn’t the final product. Parsing supports translation: converting input to another format. Compilation is the classic example—a compiler parses source code into a tree, then translates that into machine code.
Figure 7: compilation flow
A Parsing Example
Consider a simple mathematical language. Syntax rules:
Building blocks are expressions, terms, and operations.
The language can include any number of expressions.
An expression is a "term" followed by an "operation" followed by another term.
An operation is a plus or minus token.
A term is an integer token or an expression.
Analyze the input 2 + 3 - 1:
The first matching substring is 2—a term per rule #5.
expression := term operation term
operation := PLUS | MINUS
term := INTEGER | expression
A language is parseable by standard parsers if its grammar is context-free—intuitively, one that can be fully written in BNF. (For a formal definition, see the Wikipedia article on context-free grammars.)
Parser Types
Two parser types exist: top-down and bottom-up. Top-down parsers start from high-level syntactic structures and seek rule matches; bottom-up parsers start with input and gradually transform it into syntax rules, working from low-level to high-level rules.
For 2 + 3 - 1, a top-down parser first identifies 2 + 3 as an expression, then 2 + 3 - 1 as an expression (matching lower rules evolves in the process). A bottom-up parser scans input until a rule matches, substitutes the matching input with the rule, and repeats to the end. Partially matched expressions sit on the parser’s stack:
Stack
Input
2 + 3 - 1
term
+ 3 - 1
term operation
3 - 1
expression
- 1
expression operation
1
expression
-
This bottom-up type is a shift-reduce parser: input shifts right (a pointer moves from the start) while rules reduce it.
Parser Generators
Tools exist to auto-generate parsers from grammar definitions—hand-crafting an optimal parser requires deep parsing expertise, so generators save time. WebKit uses two well-known examples: Flex for creating lexers and Bison for parsers (also known as Lex and Yacc). Flex reads token regex definitions; Bison reads grammar rules in BNF format.
Why HTML Needs a Custom Parser
HTML's grammar is defined by the W3C in a formal DTD (Document Type Definition) format, but that format is not a context-free grammar. Conventional parser techniques — top-down, bottom-up, or XML-based parsers — simply don't apply. The core reason is that HTML is deliberately "soft" syntax. It forgives omitted tags, misplaced end tags, and other mistakes that a strict grammar like XML's would reject.
That leniency is precisely why HTML became so popular, but it also makes formal parsing difficult. Browsers therefore build custom parsers rather than relying on standard parser generators or existing XML tooling.
From Markup to DOM Tree
The parser's output is the DOM (Document Object Model) tree — a tree of element and attribute nodes rooted at the Document object. The DOM is a W3C specification for manipulating documents, with HTML-specific modules defining the element interfaces. While the DOM tree has an almost one-to-one correspondence with the markup, browsers use concrete internal implementations of these interfaces that carry extra attributes beyond the public DOM API.
The Two-Stage Parsing Algorithm
The HTML5 specification describes parsing as a two-stage process: tokenization followed by tree construction.
Stage 1: Tokenization
Tokenization is a lexical analysis that reads the input and produces tokens — start tags, end tags, attribute names, and attribute values. The tokenizer is expressed as a state machine. Each state consumes one or more characters and decides the next state based on the current character and the current state. Notably, the same character can lead to different outcomes depending on the tokenization state and the tree construction state.
Tokenizing <html><body>Hello world</body></html> illustrates the flow:
Starting in the "Data state," a < character transitions to the "Tag open state."
An a-z character there creates a start tag token, moving to the "Tag name state," where subsequent characters append to the token name until > is consumed.
When > is reached, the token (e.g., html, then body) is emitted and the state returns to "Data state."
Characters like those in "Hello world" each produce a character token, one per character.
A </ sequence in the "Tag open state" creates an end tag token; after consuming the name and >, that token is emitted and parsing resumes in "Data state."
Stage 2: Tree Construction
Tree construction turns the token stream into a DOM. The parser starts by creating the Document object, then processes each token according to rules that determine which DOM element it maps to. The constructor also maintains a stack of open elements to correct nesting mismatches and unclosed tags. Like tokenization, this stage is a state machine; its states are called "insertion modes."
Tracing the same example through construction:
The html start tag in the "initial mode" transitions to "before html," creating an HTMLHtmlElement appended to Document.
A body start tag (received while in "before head") triggers implicit creation of an HTMLHeadElement, even with no head token present, before moving through "in head" and "after head" modes to insert HTMLBodyElement.
Character tokens for "Hello world" create a text node; the first character creates it, subsequent characters append to it.
The body end tag moves the parser to "after body," then the html end tag moves it to "after after body," and end-of-file terminates parsing.
Three aspects of HTML make this two-stage custom approach necessary rather than relying on conventional parsers:
The forgiving nature of the language itself.
Browsers' long-established error tolerance for widely reproduced invalid HTML patterns.
Reentrancy — dynamic code like document.write() can alter the input stream mid-parse, which doesn't happen with static source in other languages.
What Happens After Parsing Completes
When parsing finishes, the browser marks the document as interactive and begins executing any scripts flagged as deferred (those meant to run after parsing). The document state then changes to "complete," and a load event fires.
Error Tolerance in Practice
A browser will never show you an "Invalid Syntax" error for HTML — it repairs broken markup and moves on. Error handling has been consistently implemented across browsers for years, though until HTML5 it wasn't formally specified. The HTML5 spec now codifies many of these repair requirements.
WebKit's parser summarizes the core error conditions it must handle:
An element explicitly forbidden inside a current outer tag: close all tags up to the forbidding tag, then add the element.
An element that cannot be added directly because a required intermediate tag (like HTML, HEAD, BODY, TBODY, TR, TD, or LI) is missing — that tag may be optional or simply forgotten.
A block element nested inside an inline element: close all inline elements up to the next higher block element.
If none of the above resolves the issue, close elements until the element can be added, or ignore the tag entirely.
Several common broken patterns get specific fixes:
</br> instead of <br>: treated as a break tag, matching IE and Firefox behavior.
A stray table: a nested table not inside a table cell is hoisted out to become a sibling table. WebKit pops the inner table off the open-element stack so both tables end up adjacent.
Nested forms: a second form inside another is simply ignored.
Excessively deep hierarchies: an internal comment notes the parser must protect against overly nested content, which likely indicates other malformed markup feeding the nesting problem.
Misplaced html or body end tags: handled by dedicated error recovery paths in the parser.
All this repair work happens internally and is never surfaced to the user. The practical takeaway for authors: write well-formed HTML, or risk starring in a browser's error-tolerance code path.
Parsing CSS: grammar-driven and fast
CSS stands apart from HTML in one crucial way: it is a context-free grammar, which makes it amenable to the classic parser-generator approach mentioned earlier. In fact, the CSS specification itself defines the lexical and syntax grammar of the language.
The lexical grammar maps each token type to a regular expression. For example, an identifier — the kind of thing you use as a class name — is matched as follows:
comment \/\*[^*]*\*+([^/*][^*]*\*+)*\/
num [0-9]+|[0-9]*"."[0-9]+
nonascii [\200-\377]
nmstart [_a-z]|{nonascii}|{escape}
nmchar [_a-z0-9-]|{nonascii}|{escape}
name {nmchar}+
ident {nmstart}{nmchar}*
Here ident is short for identifier, and name refers to an element id (the value following a # selector).
The syntax grammar, meanwhile, is expressed in BNF:
That is: a ruleset starts with one selector or a comma-separated list of selectors (with optional whitespace, denoted S), followed by curly braces containing one declaration or several separated by semicolons. The selector and declaration productions are themselves defined in subsequent BNF rules.
How the engines actually parse CSS
WebKit generates its CSS parsers automatically using Flex and Bison, yielding a bottom-up shift-reduce parser. Firefox takes a different route, using a hand-written top-down parser. Both engines end up with the same result: every CSS file is parsed into a StyleSheet object, which contains CSS rule objects. Each rule object holds selector and declaration objects, plus the other structures defined by the CSS grammar.
Figure 12: parsing CSS
Blocking behavior: scripts vs. style sheets
The web platform is, by default, synchronous where scripts are concerned. When the parser hits a <script> tag, authors expect the script to be parsed and executed immediately. That halts document parsing until execution finishes; for external scripts, the fetch itself also blocks the parser. This model held from the earliest days and is codified in both HTML4 and HTML5.
Two attributes change the default. The defer attribute tells the browser not to halt parsing and instead execute the script after the document is fully parsed. HTML5 additionally allows marking a script as asynchronous, so it is parsed and executed on a separate thread.
Speculative parsing
WebKit and Firefox both implement a speculative optimization to regain some of the time lost to synchronous scripts. While the main parser runs a script, a second thread keeps scanning the rest of the document to discover external resources — scripts, style sheets, images — and kick off their network fetches early. That lets resources load over parallel connections and improves overall speed. The speculative parser only looks for external references, though: it never touches the DOM tree, which remains the exclusive job of the main parser.
Why style sheets can halt scripts
Style sheets behave differently. Conceptually, since they don't alter the DOM tree, there is no obvious reason to pause document parsing while they load. The catch emerges when a script asks for style information mid-parse: if the relevant style sheet hasn't been loaded and parsed yet, the script gets a wrong answer. The scenario sounds like an edge case but happens frequently enough that engines guard against it.
Firefox handles this by blocking all scripts while a style sheet is still loading and parsing. WebKit is more surgical: it blocks scripts only when they attempt to access style properties that could be affected by the unloaded style sheets.
Building the visual layer
As the DOM tree takes shape, the browser constructs a second structure: the render tree, which holds the visual elements in display order. Firefox calls these elements frames; WebKit calls them renderers or render objects. A renderer knows how to lay out and paint itself and its children, and each one represents a rectangular area corresponding to a node's CSS box, with geometric data like width, height and position.
The renderer type derives from the node's display value. WebKit's RenderObject class is the base for all renderers, and its createRenderer() method can be overridden by elements that need special handling — form controls and tables get dedicated frame types. Renderers point to style objects that carry non-geometric information.
class RenderObject{
virtual void layout();
virtual void paint(PaintInfo);
virtual void rect repaintRect();
Node* node; //the DOM node
RenderStyle* style; // the computed style
RenderLayer* containgLayer; //the containing z-index layer
}
The choice of renderer is driven by the display attribute:
RenderObject* RenderObject::createObject(Node* node, RenderStyle* style)
{
Document* doc = node->document();
RenderArena* arena = doc->renderArena();
...
RenderObject* o = 0;
switch (style->display()) {
case NONE:
break;
case INLINE:
o = new (arena) RenderInline(node);
break;
case BLOCK:
o = new (arena) RenderBlock(node);
break;
case INLINE_BLOCK:
o = new (arena) RenderBlock(node);
break;
case LIST_ITEM:
o = new (arena) RenderListItem(node);
break;
...
}
return o;
}
How renderers map to the DOM
The render tree relationship to the DOM is not one-to-one. Non-visual elements like <head> never enter the render tree, nor do elements with display:none — although visibility:hidden elements do appear. Conversely, one DOM element can produce several renderers. A <select> gets three: one for the display area, one for the dropdown, and one for the button. Wrapped text lines also become individual renderers. Broken HTML can trigger the creation of anonymous block renderers when an inline element contains mixed block and inline content, per CSS requirements.
Some render objects exist at a different position in the tree than their DOM node. Floats and absolutely positioned elements are out of flow: they are placed elsewhere in the tree, with a placeholder frame marking their intended spot.
Figure 13: The render tree and the corresponding DOM tree. The "Viewport" is the initial containing block. In WebKit it will be the "RenderView" object
In Firefox, the presentation layer listens for DOM updates and delegates frame creation to the FrameConstructor, which resolves style and creates frames. WebKit calls this same process attachment: node insertion into the DOM triggers the new node's attach method synchronously. Processing the html and body tags yields the root render object — the containing block whose dimensions equal the viewport. Firefox calls this ViewPortFrame; WebKit calls it RenderView. The rest of the tree grows as nodes are inserted.
Style computation
Building the render tree means calculating the visual properties of each element from style sheets of several origins: browser defaults, author styles, and user styles such as Firefox's profile-level style sheet. Inline style attributes and HTML visual attributes like bgcolor are translated into matching CSS properties.
Style computation poses three problems. The style data is large and can strain memory. Rule matching against complex selectors can be slow. And the cascade rules that determine final values are intricate. Browsers tackle each differently.
One example of a damaging selector path — matching a <div> that is a descendant of three other divs — requires walking up the tree only to find a mismatch after a failed ancestor check:
div div div div{
...
}
Reusing style data
WebKit allows nodes to share RenderStyle objects when they are siblings or cousins and all of the following hold:
They share the same mouse, link and focus states.
Neither has an id, an inline style attribute, or attribute selectors matching.
Tag names, class attributes, and mapped attributes match.
No sibling selectors are in use anywhere — WebCore disables style sharing for the whole document when it encounters +, :first-child or :last-child.
Firefox's rule tree
Firefox maintains two additional structures for style computation: the rule tree and the style context tree. WebKit keeps style objects but stores them directly on the DOM node rather than in a tree.
Figure 14: Firefox style context tree.
Style contexts hold end values — the computed results of applying all matching rules in cascade order and performing transformations, for instance from percentage to absolute units. The rule tree enables sharing of these values, saving both computation and memory. All matched rules are stored in a tree where bottom nodes have higher priority, and storage is lazy: computed paths are added only when node styles are requested.
The tree paths behave like words in a lexicon. If a path for rules B-E-I already exists because path A-B-E-I-L was computed earlier, less work is needed for the new match. Style contexts are divided into structs by category — border, color, and so on — and each struct's properties are either inherited or reset. If a rule node doesn't supply a value for a struct, a cached struct from an ancestor rule node can be reused, producing the best-case optimization of sharing an entire struct. Partial definitions cause a walk up the tree until the struct is filled. If nothing is found, inherited structs are taken from the parent in the context tree, and reset structs fall back to default values. When the most specific node provides values, the result is computed and cached for its children. Siblings that point to the same rule tree node can share an entire style context.
Consider the following HTML and its associated rules:
<html>
<body>
<div class="err" id="div1">
<p>
this is a <span class="big"> big error </span>
this is also a
<span class="big"> very big error</span> error
</p>
</div>
<div class="err" id="div2">another error</div>
</body>
</html>
div {margin: 5px; color:black}
.err {color:red}
.big {margin-top:3px}
div span {margin-bottom:4px}
#div1 {color:blue}
#div2 {color:green}
With two structs — color and margin — the resulting trees look like this:
Figure 16: The rule treeFigure 17: The context tree
When the second <div> is parsed and matched to rules 1, 2 and 6, an existing path is extended with a node for rule 6. The margin struct is found cached on node B; color must be computed and then cached. The second <span> points to the same rule node as the first, so its style context is shared outright. Inherited structs — like fonts — can be shared down the context tree, as when a paragraph inherits a font struct from its div parent in the absence of paragraph-specific font rules:
p {font-family: Verdana; font size: 10px; font-weight: bold}
WebKit, lacking a rule tree, traverses matched declarations four times: non-important high priority properties first, then important high priority, then non-important normal and finally important normal — so the correct cascade order resolves repeated declarations, with the last one winning.
Preparing rules for faster matching
CSS rules come in three forms: external or inline style sheet rules, inline style attributes, and HTML visual attributes. The latter two attach directly to the element, but finding matching CSS rules is trickier. Both WebKit and Firefox address this by parsing each selector into hash maps keyed by id, class, and tag, with a general map for anything else. Matching then requires checking only the extracted rules for an element instead of every declaration — eliminating 95+ percent of rules from consideration.
For these rules:
p.error {color: red}
#messageDiv {height: 50px}
div {margin: 5px}
the class map gets the p.error rule, the id map the #mydiv rule, and the tag map the td rule. Matching the following fragment picks the appropriate lists, though the rightmost selector key does not guarantee a true match:
<p class="error">an error occurred</p>
<div id=" messageDiv">this is a message</div>
table div {margin: 5px}
A rule keyed by tag but requiring a table ancestor would still be extractable yet fail the ancestor check.
Cascade and specificity
The cascade order, from lowest to highest priority per CSS2, runs: browser declarations, user normal declarations, author normal declarations, author important declarations, and user important declarations. Equal-priority declarations are resolved by specificity, then by source order. HTML visual attributes become author rules at low priority.
Specificity is defined by four counts: whether the declaration comes from a style attribute (a), the number of ID attributes (b), the number of other attributes and pseudo-classes (c), and the number of element names and pseudo-elements (d). Concatenating a-b-c-d in a base large enough to contain the largest single count gives the specificity value.
After matching, rules are sorted by cascade rules. WebKit relies on bubble sort for small lists and merge sort for large ones, overriding the comparison operator accordingly:
WebKit tracks whether all top-level style sheets, including @import rules, have loaded. If attachment runs before the style sheets are ready, placeholders stand in and the document is marked for recalculation once loading finishes.
Computing geometry
When a renderer is first created and attached to the render tree, it has no position or dimensions. The process of calculating these values is called layout (or reflow). HTML uses a flow-based layout model: geometry can usually be computed in a single pass from left to right and top to bottom, since elements later in the flow rarely affect those earlier in it. Tables are a notable exception and may require multiple passes.
Layout is recursive, starting at the root renderer for the <html> element. The root sits at coordinates (0,0) and its dimensions match the viewport. Each renderer implements a method that computes its children’s geometry. The coordinate system is relative to the root frame, using top and left coordinates.
Dirty bit system
To avoid recomputing the whole tree on every change, renderers use a dirty flag system. A renderer that is modified or inserted marks itself and its descendants as needing layout. Two flags track state: "dirty" (this renderer needs layout) and "children are dirty" (a descendant needs layout, but this renderer itself may be fine).
Global vs. incremental layout
Layout can be global — applied to the entire render tree — or incremental, affecting only dirty renderers. Global layout is triggered by changes like a font size change that affects all renderers, or a viewport resize. Incremental layout fires asynchronously when renderers become dirty; for instance, when new content arrives from the network and is appended to the DOM, new renderers are added to the tree with dirty marks.
Figure 18: Incremental layout - only dirty renderers and their children are laid out
Firefox queues reflow commands and a scheduler executes them in batches. WebKit likewise uses a timer to traverse the tree and lay out dirty renderers. Scripts that query style information — such as offsetHeight — force incremental layout to run synchronously, and global layout is normally synchronous too. Layout can also run as a callback after an initial pass, for example after a scrolling position changes.
Some optimizations reduce the work. When a resize or a positional change triggers layout, cached widths may be reused rather than recomputed. Layout can also start from a subtree when a change is local, such as text inserted into a text field, which would otherwise trigger a full relayout from the root per keystroke.
The layout process
The parent renderer determines its own width.
The parent iterates over children: setting each child's x and y, and calling child layout when the child is dirty or during a global pass to compute its height.
The parent sums children's heights with margins and padding to derive its own height, which the chain above will use.
The parent clears its dirty bit.
Firefox passes a state object (nsHTMLReflowState) to its reflow implementation; the object carries the parent width. The result comes back in a metrics object (nsHTMLReflowMetrics) containing the computed height.
Width calculation
A renderer's width is derived from the containing block's width, the style width property, and the margins and borders. WebKit's RenderBox method calcWidth performs this for a div:
<div style="width: 30%"/>
The container width is the greater of availableWidth and 0; in this case availableWidth equals contentWidth, which is:
clientWidth() - paddingLeft() - paddingRight()
clientWidth and clientHeight describe the interior of an object, excluding borders and scrollbars.
The element's width comes from the width style attribute, converted to an absolute value by computing the percentage of the container width.
Horizontal borders and padding are then added.
This yields the "preferred width". The engine then checks minimum and maximum bounds: if preferred exceeds the maximum, the maximum is used; if it falls below the minimum (the smallest unbreakable unit), the minimum is applied. Results are cached in case layout is needed again without a width change.
Line breaking
When a renderer determines mid-layout that it must break, it halts and signals its parent. The parent creates any additional renderers needed and invokes layout on them.
Painting
During painting, the render tree is traversed and each renderer's paint() method draws content to the screen through the UI infrastructure layer. Painting can be global, covering the entire tree, or incremental. In the incremental case, a changed renderer invalidates its screen rectangle. The OS identifies this as a dirty region and fires a paint event, coalescing multiple regions where possible. Chrome’s renderer runs in a separate process from the main one, so it simulates this OS behavior at the application level.
Paint order follows the CSS2 stacking rules: stacks are drawn back-to-front, so within a block renderer the sequence is background color, background image, border, children, then outline. Firefox builds a display list for the affected rectangle, containing the relevant renderers in the correct paint order, so a repaint requires only one tree traversal instead of repeated passes for backgrounds, images, and borders. As an optimization, Firefox skips elements that will be hidden, such as those fully covered by opaque elements. WebKit instead stores the previous rectangle as a bitmap and repaints only the delta between old and new rectangles.
Dynamic changes and the event loop
Browsers attempt the minimum work needed for a change. A color change repaints just that element; a position change relayouts and repaints the element, its children, and possibly its siblings; a DOM node addition relayouts and repaints the node; and major changes like resizing the <html> font invalidate caches, forcing relayout and repaint of the whole tree.
The rendering engine runs on a single thread — Firefox and Safari use the browser's main thread, Chrome uses the tab process's main thread — while network operations may run on several parallel threads (typically 2-6 connections). The main thread runs an event loop, waiting for and dispatching layout and paint events:
while (!mExiting)
NS_ProcessNextEvent(thread);
The CSS2 visual model
Canvas and box model
Per the CSS2 specification, the canvas is the space where the formatting structure is rendered. It is infinite in each dimension, but browsers choose an initial width based on the viewport. A canvas nested inside another is transparent; a standalone canvas takes a browser-defined background color.
The CSS box model defines rectangular boxes each element generates: a content area (text, image, etc.) surrounded by optional padding, border, and margin.
Figure 19: CSS2 box model
Every node generates zero or more boxes. The display property determines the box type:
block: generates a block box.
inline: generates one or more inline boxes.
none: no box is generated.
The default display value is inline, though browser style sheets often override it — a div defaults to block.
Positioning schemes and box types
Three positioning schemes exist. Normal flow places objects according to their document order, matching positions in the DOM and render trees. Float positions an object in normal flow, then shifts it as far left or right as possible. Absolute positioning places an object in the render tree at a position unrelated to its DOM position. The position property and float attribute select the scheme: static and relative use normal flow, while absolute and fixed invoke absolute positioning. Static positioning requires no explicit coordinates; the others accept top, bottom, left, and right values.
A box's layout depends on its type, dimensions, positioning scheme, and external information such as image size or screen dimensions. Block boxes own a rectangle in the browser window:
Figure 20: Block box
Inline boxes instead sit inside a containing block:
Inline boxes are placed within line boxes, which are at least as tall as their tallest member — taller when boxes are baseline-aligned, so one element's bottom aligns with another's interior point. When a container's width runs out, inlines wrap onto multiple lines, as in normal paragraphs:
Figure 23: Lines
Specific positioning modes
Relative positioning first lays the box as usual, then offsets it by the specified delta:
Figure 24: Relative positioning
Float boxes shift left or right within a line, and other boxes flow around them. Given the HTML:
<p>
<img style="float: right" src="images/image.gif" width="100" height="100">
Lorem ipsum dolor sit amet, consectetuer...
</p>
the result is:
Figure 25: Float
Absolute and fixed positioning ignore normal flow entirely; dimensions are computed relative to the containing block, and for fixed positioning, the container is the viewport:
Figure 26: Fixed positioning
Stacking and z-index
The z-index property adds a third dimension: a box's position along the z-axis. Boxes are grouped into stacking contexts. Within a stack, background elements paint first, and overlapping boxes hide those behind them. The stacks themselves are ordered by z-index; any box with a z-index forms a local stack, and the viewport provides the outer stack.
Here the red div precedes the green one in the document, which would paint it first in normal flow — but its higher z-index places it closer to the viewer in the root stack.
Further Reading and References
Architecture and Parsing
For a deeper look at browser internals, the classic starting points remain Alan Grosskurth's "A Reference Architecture for Web Browsers" and Vineet Gupta's multi-part series on browser architecture. On the parsing side, the standard reference is the "Dragon book" (Aho, Sethi, Ullman, Compilers: Principles, Techniques, and Tools, Addison-Wesley, 1986), while Rick Jelliffe's article on the HTML 5 drafts covers the tokenization and tree-construction design decisions.
Firefox and Gecko
Mozilla's engineers have published detailed documentation of the Gecko layout engine. L. David Baron's materials on "Faster HTML and CSS" — available as both slides and a Google tech talk video — cover layout engine internals for web developers. Baron also maintains the Mozilla Layout Engine overview and the Mozilla Style System Documentation.
Chris Waterson contributed two key pieces: Notes on HTML Reflow and the Gecko Overview. Alexander Larsson's "The life of an HTML HTTP request" provides a useful end-to-end trace.
About a year ago, I was offered a presentation slot at the WeAreDevelopers World Congress in Berlin. I rarely take speaking engagements, especially international ones, but this one arrived at just the right time, the right place, and with the right person – I said yes, on the contingency that Ben Dumke-von der Ehe joins me in the presentation. Ben is an early community hire at Stack Overflow who l