How WebCore Handles HTML Parsing and Layout in Safari

This article provides a technical overview of how the WebCore component within the WebKit engine processes web pages for the Safari browser. It covers the essential stages of preparing a webpage for display, specifically detailing how WebCore parses raw HTML into a Document Object Model (DOM), combines it with CSS styles, and executes the layout phase to determine the exact size and position of elements on the screen.

HTML Parsing and DOM Construction

The rendering process in WebCore begins the moment raw bytes of an HTML document arrive from the network. WebCore converts these bytes into characters based on the file's specified encoding and passes them to the HTMLDocumentParser.

Parsing is divided into two main sub-processes: tokenization and tree construction.

  1. Tokenization: The HTMLTokenizer breaks the character stream into distinct tokens, such as start tags, end tags, attribute names, and text content. It uses a state machine to handle the complexities and forgiving nature of HTML syntax, ensuring even broken HTML is parsed predictably.
  2. Tree Construction: The HTMLTreeBuilder takes the tokens generated by the tokenizer and constructs the Document Object Model (DOM) tree. Each token is converted into a specific Node or Element C++ object in memory. This tree represents the hierarchical structure of the web page and serves as the foundation for both rendering and JavaScript manipulation.

If the parser encounters a blocking resource, such as a synchronous <script> tag, the HTMLDocumentParser halts tree construction until the script is fetched and executed. To prevent network bottlenecks during this pause, WebCore uses a lightweight HTMLPreloadScanner to look ahead in the unparsed HTML and speculatively fetch external resources like images and stylesheets.

Style Resolution

Before WebCore can calculate the layout of a page, it must know what visual rules apply to each element. As the DOM is being built, WebCore simultaneously parses CSS files and <style> blocks into internal style sheets.

Once the DOM and CSS rules are processed, WebCore performs style resolution. The StyleResolver matches CSS selectors against DOM nodes to compute the final set of CSS properties for every element. The result of this computation is stored in an object called RenderStyle, which holds all the visual and geometric properties—such as margins, display types, and dimensions—for a given DOM node.

Constructing the Render Tree

With the DOM tree and RenderStyle objects in place, WebCore builds the Render Tree. While the DOM tree represents the document's structure and content, the Render Tree represents what will actually be drawn on the screen.

The Render Tree is composed of RenderObject nodes. For most visible DOM nodes, WebCore creates a corresponding RenderObject and attaches the computed RenderStyle to it.

The DOM tree and the Render Tree do not have a one-to-one mapping:

  • Non-visual DOM elements, such as <head>, <script>, or elements with the CSS property display: none, are omitted from the Render Tree entirely.
  • Pseudo-elements, like ::before and ::after, do not exist in the DOM, but they are inserted as physical nodes in the Render Tree.
  • Elements with visibility: hidden are included in the Render Tree because they still take up physical space in the layout, even though they are not visibly painted.

The Layout Phase

Once the Render Tree is constructed, WebCore moves to the layout phase. At this point, the RenderObject nodes exist, but they do not have calculated positions or dimensions. The goal of the layout phase is to compute the exact coordinates and bounding boxes for every node in the Render Tree based on the browser's viewport size.

Layout is a recursive process that begins at the root of the Render Tree, known as the RenderView, which corresponds to the Safari window's visible viewport.

  1. Parent-to-Child Traversal: The RenderView determines the available width based on the browser window and passes this constraint down to its child renderers.
  2. Box Model Calculation: Each RenderBox (a subclass of RenderObject for elements that adhere to the CSS box model) calculates its own width, height, margins, padding, and borders. Generally, an element's width is constrained by its parent, while its height is determined by the cumulative height of its children.
  3. Formatting Contexts: WebCore uses different layout algorithms depending on the CSS display properties in use. It contains specific logic for standard block and inline flows, tables, Flexbox (RenderFlexibleBox), and CSS Grid (RenderGrid). WebKit also utilizes a modern architecture called Layout Formatting Contexts (LFC), which calculates inline and block layout geometry using an immutable tree structure to improve speed and reliability.

Incremental Layout and Dirty Bits

Recalculating the layout of an entire webpage is computationally expensive. If a user resizes a single element or JavaScript injects new text, WebCore avoids laying out the whole page from scratch by using a "dirty bit" system.

When a change occurs that affects geometry, WebCore marks that specific RenderObject and its ancestor chain as "dirty" (needing layout). During the next layout pass, WebCore traverses the Render Tree from the root, ignoring branches that are not marked as dirty, and only recalculates the geometry for the affected subtrees.

Once the layout phase completes and all RenderObject nodes have valid coordinates and dimensions, WebCore passes the updated tree to the painting and compositing phases, which finally draw the pixels into the Safari window.