Back to Blog
Frontend11 min readJun 2026

How the Browser Renders a Page

From the raw HTML bytes to the pixels on your screen: the critical rendering path explained, DOM, CSSOM, render tree, layout, paint, composite, and why reflows quietly wreck performance.

FrontendBrowserRenderingPerformance
SB

Sri Balaji

Founder · TheSimplifiedTech

On this page

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).

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 togetherDOM + CSSOM merged into the render tree
Laying pieces out on the floorLayout, computing position and size
Painting and staining each piecePaint, filling in pixels, colours, shadows
Slotting finished sections togetherComposite, 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.

HTML

Source bytes

DOM

Object tree

Render Tree

Visible nodes

Layout

Position + size

Paint

Fill pixels

Composite

Final frame

CSS

Source bytes

CSSOM

Style tree

The critical rendering path: from source files to pixels on screen.

  1. 1

    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.

  2. 2

    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.

  3. 3

    DOM + CSSOM → Render Tree

    The two trees are merged. The render tree contains only what will actually be drawn: nodes with `display: none` are dropped entirely, and each remaining node carries its computed styles. `<head>`, `<script>`, and hidden elements never make it in.

  4. 4

    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.

  5. 5

    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.

  6. 6

    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.

StageWhat happensWhat makes it slow
DOM buildHTML parsed into a node treeHuge or deeply nested markup; thousands of elements
CSSOM buildStylesheets parsed into style rulesLarge CSS files; complex, deeply nested selectors
Render treeVisible DOM nodes get computed stylesMostly proportional to DOM + CSSOM size
LayoutGeometry of every box computedTouching layout-affecting properties; large DOM; reading layout mid-write
PaintPixels filled per layerLarge paint areas; expensive effects (shadows, blur, gradients)
CompositeLayers stacked on the GPUToo 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
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 before DOMContentLoaded. 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 <head> 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 transform or opacity and the browser can skip both layout and paint, just re-stacking existing layers on the GPU. This is why smooth 60fps animations stick to transform and opacity.

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
javascript
// ❌ 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

  1. Render-blocking scripts in `<head>`. A plain <script src> before your content stalls parsing and delays first paint. Add defer, or move it to the end of <body>.
  2. Layout thrashing in loops. Interleaving DOM reads (offsetTop, getBoundingClientRect) and writes forces a reflow per iteration. Batch all reads, then all writes.
  3. Animating layout properties. Animating width, top, or margin reflows every frame and drops your frame rate. Animate transform and opacity instead.
  4. A huge DOM. Tens of thousands of nodes make every reflow and style recalculation slow. Virtualize long lists and prune unnecessary wrapper elements.
  5. 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 `defer` for app code, `async` for independent scripts.
  • Reflow (layout) is the most expensive change, repaint is cheaper, composite-only is cheapest.
  • Animate `transform` and `opacity` to 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.

Want to go deeper?

This article covers concepts taught hands-on in the Cloud Engineer and DevOps career paths, with real terminal labs, production scenarios, and structured lessons.