What you'll learn
- Quick answer
- The Pipeline Behind Every Page Load
- Step 1: HTML Parsing and DOM Construction
- Step 2: CSS Parsing and the CSSOM
- Step 3: Render Tree Construction
- Step 4: Layout (Reflow) — Computing Where Everything Goes
- Step 5: Paint — Turning Boxes Into Pixels
- Step 6: Compositing — and Why Transform/Opacity Animations Are Different
- FAQ
Quick Answer
The critical rendering path is the fixed sequence a browser runs to turn HTML, CSS, and JavaScript into pixels: parse HTML into a DOM, parse CSS into a CSSOM, combine them into a render tree, compute layout (position and size of every element), paint pixels for each layer, then composite those layers together on screen. Every millisecond spent before that first composite is a millisecond your user spends looking at a blank or unstyled page.
The Pipeline Behind Every Page Load
A browser doesn't display a page the moment bytes arrive over the network — it runs a fixed pipeline first: parse the HTML into a DOM, parse the CSS into a CSSOM, merge those into a render tree, compute layout, paint pixels, and composite the result on screen. This sequence is called the critical rendering path, and understanding its actual mechanics is what separates "add defer to your script tag because a blog post said so" from actually knowing which of the six steps that fixes and why.
This site already covers the outcomes of getting this pipeline right or wrong — Core Web Vitals, font loading strategy, image optimization, INP — each in its own dedicated post. This one is about the mechanism underneath all of them: the actual sequence of steps a browser's rendering engine runs, in order, every single time a page loads or meaningfully changes.
Step 1: HTML Parsing and DOM Construction
The browser's HTML parser reads bytes as they arrive and incrementally builds the DOM (Document Object Model) — a tree of node objects mirroring the tag structure of the page. This is genuinely incremental and streaming: the parser doesn't wait for the entire HTML document to download before starting, which is why a large page's top content can start appearing before the bottom of the file has even arrived over the network.
Two things can interrupt this parser mid-stream. A <script> tag with no async or defer attribute blocks HTML parsing entirely the moment the parser reaches it — the browser has to stop, fetch the script if it's external, execute it fully, and only then resume parsing the rest of the HTML, because that script might call document.write() and change what comes next. This is the actual mechanical reason "put scripts at the bottom of the body" became a rule of thumb before defer existed, and why async/defer exist at all: both let the browser keep parsing HTML while the script downloads in the background, differing only in when the downloaded script is allowed to run (as soon as it's ready, for async; only after parsing finishes, in document order, for defer).
The browser also runs a preload scanner in parallel with the main parser — a separate, lightweight pass that looks ahead in the raw HTML for resources worth starting downloads for early (images, stylesheets, scripts) even before the main parser has reached that point in the document. This is why moving a critical resource further down the HTML doesn't always delay its download as much as you'd expect — the preload scanner may have already found it.
Step 2: CSS Parsing and the CSSOM
Every stylesheet — external files, <style> blocks, inline style attributes — gets parsed into the CSSOM (CSS Object Model), a tree of rules that, unlike raw source order, represents the fully resolved cascade: for every selector, which declarations actually apply after specificity, source order, and !important are all resolved.
CSS is render-blocking: the browser will not paint any content until CSSOM construction for all stylesheets discovered so far is complete, because it has no way to know whether a stylesheet still loading might change how already-parsed elements should look. This is a deliberate, conservative choice to avoid a flash of incorrectly styled content, and it's why an external stylesheet linked in the <head> without any special loading attribute delays first paint until it genuinely finishes downloading and parsing, regardless of how fast the rest of the page loads.
Note the distinction from step 1: CSS blocks rendering but not HTML parsing — the DOM keeps being built in the background while a stylesheet downloads; it's only the point where pixels can be painted that waits.
Step 3: Render Tree Construction
The render tree is built by walking the DOM and, for each visible node, attaching the computed styles it needs from the CSSOM. "Visible" is doing real work in that sentence: any element with display: none, and its entire subtree, is left out of the render tree completely — it exists in the DOM (JavaScript can still query and modify it) but doesn't participate in layout or paint at all. This is different from visibility: hidden, which does keep the element in the render tree (it still takes up layout space, invisibly) — a distinction worth knowing precisely because the two are so often used interchangeably in casual conversation despite having genuinely different rendering costs.
The render tree also includes generated content that has no corresponding DOM node at all, like text inserted via a CSS ::before/::after pseudo-element's content property — the render tree is shaped by what needs to be drawn, not strictly by what exists in the document's markup.
Step 4: Layout (Reflow) — Computing Where Everything Goes
Layout takes the render tree and computes the exact pixel position and size of every node, starting from the root and working down (and, for anything involving flexbox or grid's more complex sizing negotiation, sometimes iterating). The output is a box for every visible node with an exact x, y, width, and height — this step is also called reflow when it happens again after the initial layout, triggered by anything that could change an element's geometry: adding or removing DOM nodes, changing an element's width or content, resizing the browser window, or even just a requested font loading and changing text metrics.
The costly mistake here is layout thrashing (also called forced synchronous layout): writing to the DOM, then immediately reading a geometry property like offsetWidth or getBoundingClientRect(), then writing again, in a tight loop. Each read after a write forces the browser to run layout synchronously right then, instead of batching all the pending style changes and running layout once — turning what should be one reflow into dozens, all inside a single JavaScript task. The fix is mechanical, not clever: batch all your DOM reads together, then batch all your writes together, so layout only has to run once per frame instead of once per read.
Step 5: Paint — Turning Boxes Into Pixels
Paint takes the boxes computed by layout and actually rasterizes them — filling in pixels for backgrounds, borders, text, shadows, and images, for every visible box. Modern browsers don't paint the whole page as one flat image; certain CSS properties (like a declared will-change, a 3D transform, position: fixed, or video and canvas elements) cause the browser to promote an element onto its own layer, painted independently of the rest of the page.
Layers matter because paint is one of the more expensive steps per pixel affected, and splitting the page into layers means changing one small layer (a fixed header, say) only requires repainting that layer, not the entire page. Overusing layer promotion has its own cost, though — each layer consumes GPU memory, and a page with dozens of unnecessary layers can end up slower than one with none, from memory pressure alone.
Step 6: Compositing — and Why Transform/Opacity Animations Are Different
Compositing takes every painted layer and combines them in the correct order (respecting stacking context, transparency, and 3D transforms) into the final image shown on screen — this final step runs on the GPU-backed compositor thread, separate from the main JavaScript thread, which is exactly why it can keep running smoothly even while JavaScript is busy doing something else.
This is also where the well-known animation performance advice comes from, and it's worth knowing the actual mechanism rather than just the rule: animating transform or opacity can skip layout and paint entirely, going straight to a re-composite of already-painted layers — the compositor thread just recalculates a matrix and blends layers with a new opacity value, with no CPU-side layout math or per-pixel repainting involved. Animating almost anything else — width, top/left, margin, box-shadow — triggers layout, paint, and composite all over again, on every single frame of the animation, which is the actual reason those animations feel janky on constrained devices while transform-based ones stay smooth even on cheap phones.
This is also the direct mechanical link back to Core Web Vitals: a page that forces synchronous layout on every scroll or input handler is exactly the kind of page that scores badly on INP, and a page where layout keeps recomputing because images and ads have no reserved space is exactly the kind of page that scores badly on CLS — both scores are measuring the visible symptoms of this pipeline running more often, and more expensively, than it needs to.
