Critical Rendering Path
How the browser turns HTML, CSS, and JS into pixels — parsing, DOM/CSSOM, render tree, layout, paint, composite.
The browser receives a pile of HTML, CSS, and JavaScript and somehow turns it into colored pixels on your screen. The exact sequence it follows is the Critical Rendering Path (CRP) — and once you can see each step, you can see exactly where it's safe to save time and where you're accidentally blocking the whole thing.
From bytes to pixels, stage by stage
The CRP has two halves: parsing (turn the downloaded files into in-memory trees) and rendering (turn those trees into pixels). Walk through all six stages on a tiny example page and watch the viewport fill in only at the very end.
Parsing: the DOM and the CSSOM
As HTML bytes arrive, the browser tokenizes them — recognizing <html>, <body>, <h1> — and assembles a tree starting from the root: the DOM. More nodes means more time, so a giant DOM is itself a performance cost.
CSS is parsed into a parallel structure, the CSSOM: the same tree shape, but each node carries its computed styles, resolved by the cascade from general rules to specific ones. Building it is typically only a few milliseconds even on big sites.
<link> or <img>, it's often already downloading. This is also why where you put resource hints matters (a later lesson).Render-blocking resources
Here's where the path stalls. The browser cannot paint until it has the CSSOM (an unstyled flash would be unacceptable), so an external stylesheet is render-blocking. Worse, a plain <script> is parser-blocking: the browser stops building the DOM, downloads and runs the script, then resumes. Toggle the options and watch the First-Paint marker move.
The fixes, in code
<!-- Blocks the parser: avoid in <head> -->
<script src="/app.js"></script>
<!-- Downloads in parallel, runs after parsing, keeps order -->
<script src="/app.js" defer></script>
<!-- Downloads in parallel, runs ASAP (order not guaranteed) -->
<script src="/analytics.js" async></script>What actually makes it into the render tree
The render tree is DOM + CSSOM, filtered to visible nodes only. This is where display:none elements vanish completely — they stay in the DOM but never reach layout or paint. That's different from visibility:hidden, which is in the render tree and does take up space. Flip between them:
Layout, paint, and composite
With the render tree in hand, three steps finish the job:
Layout computes the exact position and size of every box, relative to the viewport. The first pass is “layout”; any later recalculation triggered by a change (a resize, a DOM mutation, reading an element's size) is a reflow — and reflows are expensive because they can cascade across the page.
Paint fills in the pixels — text, colors, borders, images. The first pixels to land are the First Paint. Composite then layers overlapping elements in the correct order (using hints like z-index and GPU layers) to produce the final frame.
| Layout / reflow | Paint | |
|---|---|---|
| Computes | Position & size of boxes | Actual colored pixels |
| Triggered by | Geometry changes (size, position, DOM) | Appearance changes (color, shadow) |
| Relative cost | Expensive — can cascade | Cheaper, but still real |
| Cheapest to animate | transform / opacity skip layout & paint — composite only |
left or width forces layout + paint every frame. Animating transform or opacity can be handled by the compositor alone — no layout, no paint — which is why smooth 60fps animations stick to those two properties.display:none. Where does it appear?- →The CRP is parse (DOM + CSSOM) → render (render tree → layout → paint → composite).
- →External CSS is render-blocking; a plain script is parser-blocking. Inline critical CSS; use
defer/asyncfor scripts. - →The render tree holds only visible nodes —
display:noneis excluded entirely. - →Reflows (re-layout) are expensive and can cascade; minimize what triggers them.
- →Animate
transform/opacityto stay on the compositor and skip layout + paint.