How browsers render a page
Between receiving HTML and showing pixels, the browser runs a pipeline called the critical rendering path. Knowing its stages explains most frontend performance advice.
The pipeline
- Parse HTML into the DOM. The parser reads bytes, turns them into tokens and builds the Document Object Model, a tree of nodes.
- Parse CSS into the CSSOM. Every stylesheet,
<style>block and inline style becomes the CSS Object Model. - Build the render tree. The browser combines DOM and CSSOM and keeps only what is visible. Elements with
display: noneare left out; elements withvisibility: hiddenstay in, because they still take up space. - Layout (also called reflow). The browser computes the exact position and size of every box, starting from the viewport width.
- Paint. It fills in pixels: text, colours, borders, shadows, images.
- Composite. Painted layers are drawn in the right order, often on the GPU.
What blocks rendering
CSS is render-blocking. The browser will not paint until it has the CSSOM, because painting with half the styles would cause a flash of unstyled content.
A classic <script> is parser-blocking. When the parser meets one, it stops building the DOM, downloads the script (if external) and runs it, since the script might call document.write or read the DOM so far. A script also waits for any pending stylesheets, because it might read computed styles.
<!-- Blocks parsing: download + execute before continuing -->
<script src="/app.js"></script>
<!-- Downloads in parallel, runs after parsing, in document order -->
<script src="/app.js" defer></script>
<!-- Downloads in parallel, runs as soon as it arrives, in any order -->
<script src="/analytics.js" async></script>
Use defer for scripts that need the DOM, and async for independent scripts such as analytics. Module scripts (type="module") are deferred by default.
Reflow versus repaint
Changing a property re-runs the pipeline from the first stage it affects:
- Geometry changes (
width,height,top,font-size, adding nodes) trigger layout, then paint and composite. This is the most expensive case, because one box moving can move many others. - Visual-only changes (
color,background-color,box-shadow) skip layout and trigger paint and composite. transformandopacitycan often skip both layout and paint and run only in the compositor. That is why smooth animations use them instead oftoporleft.
Layout thrashing
Reading a layout property such as offsetHeight forces the browser to finish any pending layout first. Interleaving reads and writes in a loop forces a layout on every iteration:
// Slow: each read forces a fresh layout because the previous line wrote
for (const el of items) {
el.style.height = el.offsetHeight + 10 + "px";
}
// Fast: read everything first, then write everything
const heights = items.map((el) => el.offsetHeight);
items.forEach((el, i) => {
el.style.height = heights[i] + 10 + "px";
});
Practical takeaways
- Keep CSS small and load critical CSS early; it blocks the first paint.
- Put
deferon scripts, or load them at the end of<body>. - Animate
transformandopacity, not layout properties. - Batch DOM reads before DOM writes.
- Measure with the browser’s Performance panel before optimising. It shows exactly which stage is slow.