How Browsers Render CSS
When you open a web page, the browser does a remarkable amount of work in milliseconds to turn your HTML and CSS into the visual display you see. Understanding this pipeline — even at a high level — makes you a better CSS developer. It explains why certain animations are smooth while others are janky, why some CSS changes feel instant while others cause visible flicker, and what "render-blocking" actually means.
The critical rendering path
Browser receives HTML bytes from the server
│
▼
Parse HTML → DOM Tree
(Document Object Model — a tree of every element)
│
├──────────────────────────────────────┐
│ │
Encounter <link> for CSS Encounter <img> or <script>
│ │
▼ ▼
Fetch & parse CSS → CSSOM Tree Load in parallel
(CSS Object Model — all style rules)
│
▼
DOM + CSSOM → Render Tree
(only visible elements; display:none excluded)
│
▼
Layout (Reflow)
Calculate exact size and position of every element
│
▼
Paint
Fill pixels — colours, text, borders, shadows
│
▼
Composite
Combine painted layers (GPU-accelerated)
│
▼
Display on screenWhy CSS is render-blocking
CSS is described as render-blocking because the browser will not render anything until it has both the DOM and the CSSOM. If you have a large stylesheet that takes 500ms to download, the user sees a blank screen for 500ms. This is why:
<link rel="stylesheet">belongs in the<head>— so it starts downloading as early as possible.Keeping stylesheets small and well-split by priority matters for perceived load speed.
Critical CSS — inlining the styles needed for the visible "above the fold" content — is a real performance technique that eliminates this blank-screen delay for the first paint.
Layout, Paint, and Composite
After the render tree is built, the browser goes through three phases that are important to understand for performance:
Phase | What happens | Triggered by changing... |
|---|---|---|
Layout (Reflow) | Calculate every element's size and position |
|
Paint | Fill pixels with colour, text, borders, shadows |
|
Composite | Combine GPU layers into the final frame |
|
Layout is the most expensive phase because changing one element's size can affect the positions of dozens or hundreds of others — the browser has to recalculate the whole tree. Paint is cheaper but still significant. Composite operations happen on the GPU and are extremely fast.
This is why CSS animations that only change transform and opacity are smooth at 60fps while animations that change width or top often stutter — the former only triggers Composite, the latter triggers full Layout → Paint → Composite on every frame.
The CSSOM in practice
You rarely interact with the CSSOM directly, but it's useful to know it exists. JavaScript can read and modify it through document.styleSheets, element.style, and getComputedStyle(). When you set element.style.color = 'red' in JavaScript, you're modifying the CSSOM, which triggers a repaint of that element.
// Reading a computed style — what the browser actually applied:
const element = document.querySelector('h1')
const styles = getComputedStyle(element)
console.log(styles.fontSize) // e.g. "32px" — the resolved value
console.log(styles.fontFamily) // e.g. "system-ui, sans-serif"
// Writing a style — modifies the element's inline style:
element.style.color = 'navy' // triggers a repaint
// Reading the color you just set:
console.log(element.style.color) // "navy"
// But computed style gives you the resolved value (same here since you set it explicitly)
console.log(getComputedStyle(element).color) // "rgb(0, 0, 128)"Layer promotion and the GPU
When the browser identifies elements that change frequently (animated elements, fixed headers, sticky sidebars), it can promote them to their own compositor layer, painted separately on the GPU. Changes to that layer — like moving it with transform: translateX() — don't require Layout or Paint at all; the GPU just repositions the existing painted layer.
You can hint to the browser that an element will change with will-change:
/* Tells the browser "this element will be animated" — it may promote it
to its own layer ahead of time, reducing animation startup cost */
.animated-card {
will-change: transform;
}
/* Only use will-change on elements that genuinely will animate.
Overusing it creates many GPU layers and actually hurts performance. */What this means for your CSS
Animate
transformandopacity— they only trigger Composite, making them 60fps candidates.Avoid animating
width,height,top,left— they trigger Layout every frame.Minimise render-blocking CSS — serve critical styles inline, defer non-critical stylesheets.
Avoid reading layout properties immediately after writing them — it forces the browser to recalculate layout synchronously (a "layout thrash").
Use
will-changesparingly and intentionally — only on elements you know will animate.