⚡ TL;DR: Trace HTML bytes to pixels through the critical rendering path: DOM, CSSOM, layout, paint, composite. Once you know what blocks rendering and why reflows are expensive, you can stop layout thrashing and fix the real performance killers.
Contents
- You hit Enter, then what?
- One sentence, then a picture
- The critical rendering path
- What happens at each stage, and what slows it down
- What blocks rendering
- Reflow vs repaint vs composite (and layout thrashing)
- Common mistakes that cost hours
- Takeaways
- Where to go next
You hit Enter, then what?
You type a URL, hit Enter, and a fraction of a second later a fully styled page appears. It feels like one instant event. It isn't. Between the server sending back a blob of text and you seeing pixels, the browser runs a precise assembly line called the critical rendering path. Every stage depends on the one before it, and any stage can stall the whole pipeline.
Most performance problems, janky scrolling, a button that lags when you click it, a page that sits blank for a beat too long, are really just one of these stages doing more work than it should. Once you can name the stages, you can name the bug.
📌 Who this is for: Frontend developers who can write HTML, CSS, and JavaScript but have never looked under the hood at how they become pixels. No prior browser-internals knowledge needed. If you have ever wondered why "just adding a class" can cause a stutter, this is for you.
One sentence, then a picture
Rendering is the process of turning your HTML and CSS into a tree the browser understands, working out where everything goes, and then drawing it, in that order, every time.
Think of it like assembling flat-pack furniture. The HTML is the instruction booklet that lists every part and how they nest together. The CSS is the parts list that says which panel is which colour, which screw is which size. You cannot start screwing things together until you have read both the instructions and checked the parts. Only then can you lay the pieces out on the floor (layout), paint or stain them (paint), and finally slot the finished sections into place to form the wardrobe (composite).
| In the real world | In tech |
|---|---|
| The instruction booklet (how parts nest) | HTML parsed into the DOM tree |
| The parts list (which panel, which colour) | CSS parsed into the CSSOM tree |
| Reading instructions + parts together | DOM + CSSOM merged into the render tree |
| Laying pieces out on the floor | Layout, computing position and size |
| Painting and staining each piece | Paint, filling in pixels, colours, shadows |
| Slotting finished sections together | Composite, stacking layers into the final frame |
Building a page is building flat-pack furniture.
The critical rendering path
Here is the whole pipeline. Bytes come in on the left; pixels come out on the right. The browser never skips a step, and it processes them strictly left to right.
The critical rendering path: from source files to pixels on screen.
- HTML → DOM: The browser reads the HTML bytes, decodes them into characters, tokenizes the tags, and builds the Document Object Model, a tree of nodes where every element, attribute, and text node is an object you can read and change from JavaScript.
- CSS → CSSOM: In parallel, every stylesheet (external, internal, and inline) is parsed into the CSS Object Model, a tree of style rules. Like the DOM, it is a tree, because styles cascade and inherit down from parents to children.
-
DOM + CSSOM → Render Tree: The two trees are merged. The render tree contains only what will actually be drawn: nodes with
display: noneare dropped entirely, and each remaining node carries its computed styles.<head>,<script>, and hidden elements never make it in. - Layout (a.k.a. reflow): The browser walks the render tree and calculates the exact geometry of every node, its width, height, and x/y position on the page. This is where percentages, flexbox, and grid get resolved into real pixel boxes.
- Paint: With geometry known, the browser fills in the actual pixels: text glyphs, colours, borders, shadows, gradients, images. The output is a set of draw commands, often split across several layers.
- Composite: Finally the painted layers are stacked in the right order, respecting z-index, transforms, and opacity, and handed to the GPU to assemble the single frame you see. Smooth animations live here.
What happens at each stage, and what slows it down
Each stage has its own cost profile. Knowing which stage your change touches tells you how expensive it will be. Here is the cheat sheet.
| Stage | What happens | What makes it slow |
|---|---|---|
| DOM build | HTML parsed into a node tree | Huge or deeply nested markup; thousands of elements |
| CSSOM build | Stylesheets parsed into style rules | Large CSS files; complex, deeply nested selectors |
| Render tree | Visible DOM nodes get computed styles | Mostly proportional to DOM + CSSOM size |
| Layout | Geometry of every box computed | Touching layout-affecting properties; large DOM; reading layout mid-write |
| Paint | Pixels filled per layer | Large paint areas; expensive effects (shadows, blur, gradients) |
| Composite | Layers stacked on the GPU | Too many layers; very large layers eating GPU memory |
The six stages, what they do, and what makes them slow.
What blocks rendering
Two resource types can freeze the pipeline: CSS and JavaScript. CSS is render-blocking by default, the browser will not build the render tree (and therefore will not paint) until it has the CSSOM, because painting with the wrong styles would cause an ugly flash. JavaScript is parser-blocking: a plain <script> stops HTML parsing dead while it downloads and executes, because the script might call document.write or read the DOM.
index.html
<!-- BLOCKS: parser stops here until app.js downloads AND runs -->
<script src="/app.js"></script>
<!-- BETTER: download in parallel, run after HTML is parsed -->
<script src="/app.js" defer></script>
<!-- For independent scripts (analytics): run whenever ready -->
<script src="/analytics.js" async></script>
<!-- CSS is render-blocking: the page won't paint until this loads -->
<link rel="stylesheet" href="/styles.css" />
-
defer, download the script in the background while parsing continues, then execute it in order, just beforeDOMContentLoaded. The right default for app code. -
async, download in the background, then execute the moment it arrives, interrupting parsing. Good for independent third-party scripts that do not touch your DOM. -
No attribute, fully blocking. Avoid in
<head>unless the script genuinely must run before the page is parsed.
💡 Why CSS in the is still correct: It is tempting to think render-blocking CSS is bad, so move it down. Do not. You want CSS discovered early so the CSSOM is ready by the time the DOM is. Instead, keep the critical CSS small and load non-essential styles separately. Block early on a little, not late on a lot.
Reflow vs repaint vs composite (and layout thrashing)
After the first render, the page is not frozen, JavaScript and user interaction keep changing it. The cost of a change depends on how far back up the pipeline it forces the browser to go. This is the single most useful performance idea in this whole article.
-
Reflow (layout) is the most expensive. Change something geometric,
width,height,top,font-size, adding a DOM node, and the browser must recompute geometry, then repaint, then composite. The whole tail of the pipeline reruns. -
Repaint is cheaper. Change only appearance,
color,background,box-shadow,visibility, and geometry is untouched, so the browser skips layout but still repaints and composites. -
Composite-only is cheapest. Animate
transformoropacityand the browser can skip both layout and paint, just re-stacking existing layers on the GPU. This is why smooth 60fps animations stick totransformandopacity.
Layout thrashing is the classic trap. The browser is smart enough to batch your style writes and reflow once, unless you read a layout value in between. Reading something like offsetHeight forces the browser to flush all pending changes and reflow right now so it can give you an accurate answer. Do that inside a loop and you force a reflow on every single iteration.
thrash.js
// ❌ Layout thrashing: read -> write -> read -> write...
// Each offsetWidth read forces a synchronous reflow.
for (const box of boxes) {
const w = box.offsetWidth; // READ (forces reflow)
box.style.width = w + 10 + "px"; // WRITE (invalidates layout)
}
// ✅ Batch: read everything first, then write everything.
const widths = boxes.map((box) => box.offsetWidth); // all READS
boxes.forEach((box, i) => {
box.style.width = widths[i] + 10 + "px"; // all WRITES
});
Common mistakes that cost hours
-
Render-blocking scripts in
<head>. A plain<script src>before your content stalls parsing and delays first paint. Adddefer, or move it to the end of<body>. -
Layout thrashing in loops. Interleaving DOM reads (
offsetTop,getBoundingClientRect) and writes forces a reflow per iteration. Batch all reads, then all writes. -
Animating layout properties. Animating
width,top, ormarginreflows every frame and drops your frame rate. Animatetransformandopacityinstead. - A huge DOM. Tens of thousands of nodes make every reflow and style recalculation slow. Virtualize long lists and prune unnecessary wrapper elements.
- One giant CSS file. All of it is render-blocking. Ship the critical styles inline or small, and defer the rest so first paint is not held hostage.
Takeaways
The whole article in six lines
- The browser runs a fixed pipeline: HTML→DOM, CSS→CSSOM, render tree, layout, paint, composite.
- CSS is render-blocking; plain scripts are parser-blocking. Use
deferfor app code,asyncfor independent scripts. - Reflow (layout) is the most expensive change, repaint is cheaper, composite-only is cheapest.
- Animate
transformandopacityto stay on the cheap composite-only path. - Layout thrashing, reading a layout value between writes, forces a reflow per read. Batch reads, then writes.
- Most jank is one stage doing too much work. Name the stage, fix the bug.
Where to go next
Now that you can see the pipeline, the next step is measuring it on real pages and understanding the layout model that feeds the layout stage.
- Core Web Vitals & Frontend Performance, turn this mental model into the metrics Google actually measures.
- CSS Fundamentals: The Box Model and Layout, master the box model that the layout stage resolves into pixels.
- Follow the full Frontend Engineer path to build on these foundations step by step.
Originally published on TheSimplifiedTech, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.
Top comments (0)