HTML Parsing and DOM Construction
HTML Parsing and DOM Construction
When a browser receives HTML, it doesn't wait for the entire document before starting to render. Instead, it begins a streaming parsing process that transforms raw bytes into a structured tree.
The Parsing Pipeline
- Bytes → Characters: The browser reads raw bytes and decodes them based on the specified character encoding (UTF-8, etc.)
- Characters → Tokens: The character stream is broken into tokens (opening tags, closing tags, attributes, text)
- Tokens → Nodes: Each token produces a node in the DOM tree
- Nodes → DOM Tree: Nodes are organized into a parent-child hierarchy
Example HTML Parsing
<!DOCTYPE html>
<html lang="en">
<head>
<title>My Page</title>
</head>
<body>
<div class="container">
<h1>Hello World</h1>
<p>This is a paragraph.</p>
</div>
</body>
</html>
This produces the following DOM tree structure:
Document
├── DOCTYPE html
└── html[lang="en"]
└── head
└── title
└── "My Page"
└── body
└── div.container
├── h1
│ └── "Hello World"
└── p
└── "This is a paragraph."
Tokenization Process
The browser's HTML tokenizer works as a state machine. When it encounters <div class="container">, it:
- Recognizes the start tag token
div - Extracts the attribute
class="container" - Creates a
Divelement node - Pushes it onto the open elements stack
Incremental Rendering
Browsers don't wait for the full DOM tree. Once the <head> and enough of <body> are parsed, the browser can begin painting pixels to the screen. This is why you see content appear progressively as the page loads.
// The DOM API lets JavaScript interact with the parsed tree
document.querySelector('.container'); // Access nodes after parsing
console.log(document.body.innerHTML); // Read the tree structure
Common Parsing Behaviors
- Error recovery: Browsers handle malformed HTML gracefully, adding missing tags automatically
- Tag name case-insensitivity:
<DIV>,<div>, and<Div>all create the same element - Self-closing tags:
<br>,<img>,<input>don't need closing tags - Preprocessing: Whitespace is normalized, and entities like
&are decoded
Understanding DOM construction is essential because it determines when and how your JavaScript and CSS can interact with the page. A malformed DOM can lead to unexpected styling, broken scripts, and poor accessibility.
The Critical Rendering Path
The Critical Rendering Path
The Critical Rendering Path (CRP) is the sequence of steps browsers take to convert HTML, CSS, and JavaScript into visual pixels on screen. Understanding this path is crucial for optimizing page load performance.
CRP Steps
- DOM Construction: HTML bytes are parsed into the Document Object Model
- CSSOM Construction: CSS bytes are parsed into the CSS Object Model
- Render Tree: DOM + CSSOM combine, excluding invisible elements
- Layout: Calculate the exact position and size of each element
- Paint: Fill pixels on screen
- Composite: Arrange layers for final display
Render-Blocking Resources
<!-- This CSS file BLOCKS rendering until fully downloaded and parsed -->
<link rel="stylesheet" href="styles.css">
<!-- The browser won't paint any content until this CSS is processed -->
<p>This paragraph waits for styles.css</p>
CSS is render-blocking because the browser needs to know the final styles before it can accurately lay out and paint elements. Painting before CSS arrives would cause a flash of unstyled content (FOUC).
Script Loading Strategies
<!-- Blocking: stops HTML parsing, waits for download + execute -->
<script src="app.js"></script>
<!-- Non-blocking parsing: downloads in parallel, defers execution -->
<script src="app.js" defer></script>
<!-- Non-blocking: executes as soon as downloaded, may block DOMContentLoaded -->
<script src="analytics.js" async></script>
defer: The script downloads in parallel with HTML parsing. Execution happens after the DOM is fully parsed but before DOMContentLoaded fires. Scripts execute in order.
async: The script downloads in parallel and executes immediately upon download completion. HTML parsing pauses during execution. No guarantee of execution order.
Render Tree Construction
The Render Tree contains only visible elements. Elements with display: none or that are not in the document (like <head>) are excluded. However, elements with visibility: hidden are included but invisible.
/* These elements are excluded from the render tree */
.hidden { display: none; } /* Not in render tree */
/* These elements ARE in the render tree but invisible */
.invisible { visibility: hidden; } /* Takes space, but not visible */
Optimization Strategies
- Minimize critical CSS: Inline only the CSS needed for above-the-fold content
- Use
deferfor non-critical scripts: Avoid blocking the parser - Preload key resources:
<link rel="preload">hints the browser to fetch early - Lazy load non-visible content: Images below the fold can load later
<link rel="preload" href="fonts.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="hero-image.webp" as="image">
<script defer src="main.js"></script>
<img src="product.jpg" loading="lazy" alt="Product">
The Critical Rendering Path is the foundation of web performance optimization. Every millisecond the CRP takes is a millisecond your user waits to see content.
Layout, Paint, and Composite
Layout, Paint, and Composite
Once the browser has the Render Tree, it must convert the abstract tree structure into actual pixels on screen. This happens in three main stages: Layout, Paint, and Composite.
Layout (Reflow)
Layout calculates the exact geometry of every visible element — position (x, y), size (width, height), and how elements affect each other. This stage is also called reflow because changing any geometric property triggers a recalculation.
/* These properties trigger Layout/Reflow */
width, height, padding, margin, border
position, top, left, right, bottom
display, float, clear, grid, flex
font-size, line-height, text-align
// JavaScript that triggers expensive layout
const elem = document.querySelector('.box');
const rect = elem.getBoundingClientRect(); // Forces layout recalculation
const height = elem.offsetHeight; // Forces layout recalculation
// Reading layout after writing causes layout thrashing
for (let i = 0; i < 1000; i++) {
elem.style.height = `${i}px`; // WRITE: invalidates layout
console.log(elem.offsetHeight); // READ: forces layout recalculation
}
Paint
Paint fills the calculated geometry with actual pixels. Each element can have multiple paint operations: background, text, borders, shadows, images. The browser builds a paint list of draw instructions.
/* These properties trigger Paint but NOT Layout */
color, background-color, visibility
box-shadow, border-radius, text-shadow
background-image, opacity
// Paint can be expensive with complex visual effects
box.style.boxShadow = '0 10px 30px rgba(0,0,0,0.3)';
box.style.background = 'linear-gradient(to right, #ff0, #f0f)';
Composite
Composite arranges one or more layers onto the screen. The browser can promote elements to their own compositing layer for better performance.
/* These properties trigger compositing */
transform: translateZ(0); /* Promote to new layer */
opacity (animated), will-change: transform;
/* GPU-accelerated properties (composite only) */
transform, opacity, filter
Repaint vs Reflow
| Operation | Cost | What Changes |
|---|---|---|
| Layout (Reflow) | High | Element geometry (size, position) |
| Paint (Repaint) | Medium | Visual appearance (colors, shadows) |
| Composite | Low | Layer arrangement |
Minimizing Layout Thrashing
Layout thrashing occurs when JavaScript repeatedly reads layout properties then writes, forcing synchronous reflows.
// BAD: Layout thrashing
for (let i = 0; i < items.length; i++) {
items[i].style.width = `${items[i].offsetWidth * 2}px`; // READ + WRITE per item
}
// GOOD: Batch reads, then batch writes
const widths = items.map(item => item.offsetWidth); // READ all
items.forEach((item, i) => {
item.style.width = `${widths[i] * 2}px`; // WRITE all
});
// BETTER: Use CSS classes instead of inline styles
items.forEach(item => item.classList.add('doubled-width'));
The Complete Rendering Pipeline
DOM Tree + CSSOM Tree
↓
Render Tree
↓
Layout (Geometry: position, size)
↓
Paint (Pixels: colors, text, images)
↓
Composite (Layers: final screen arrangement)
↓
Pixels on Screen
Each stage is a potential performance bottleneck. Optimizing requires understanding which operations trigger which stages and batching DOM changes to minimize expensive layout recalculations.
Quiz
1. What does the browser create after parsing HTML?
2. Which resource type blocks rendering until it's fully downloaded?
Flashcards
Question
What is the Critical Rendering Path?
Click to reveal answer
Answer
The sequence of steps the browser takes to convert HTML/CSS into pixels: HTML → DOM → CSSOM → Render Tree → Layout → Paint → Composite.
Question
What is the difference between reflow and repaint?
Click to reveal answer
Answer
Reflow recalculates element geometry (position, size). Repaint fills pixels for visual changes. Reflow is more expensive than repaint.
Revision Notes
Key Takeaways
- 1. HTML parsing creates the DOM tree — the browser's representation of the page structure
- 2. CSS is render-blocking; JavaScript can be made non-blocking with defer/async
- 3. The Critical Rendering Path: DOM + CSSOM → Render Tree → Layout → Paint → Composite
- 4. Reflows (geometry changes) are more expensive than repaints (visual changes)
Interview Tips
- • Explain the Critical Rendering Path step by step
- • Know the difference between defer and async script loading
- • Discuss how to minimize reflows and repaints
Cheat Sheet
Browser Rendering
- DOM: HTML → tree structure
- CSSOM: CSS → style rules
- Render Tree: DOM + CSSOM (visible elements only)
- Layout: Calculate geometry
- Paint: Fill pixels
- Composite: Arrange layers
- Reflow: Geometry change (expensive)
- Repaint: Visual change (cheaper)