How Browsers Render a Page

From HTML text to pixels on screen. The critical rendering path (DOM, CSSOM, layout, paint, composite) and why it matters for fast, smooth pages.

Beginner⏱ 4 min readLesson 1 of 11#frontend#browser#rendering#performance#dom

The big idea

Building a page is like building a house:

  1. Read the blueprint (HTML) β†’ know which rooms exist (DOM).
  2. Read the interior design notes (CSS) β†’ know the colours and sizes (CSSOM).
  3. Combine them into a plan of what's actually visible (render tree).
  4. Measure where every wall goes (layout).
  5. Paint the walls (paint).
  6. Stack the floors together (composite).

The critical rendering path: from HTML and CSS to pixelsThe critical rendering path: from HTML and CSS to pixels

Step by step

Drawing diagram…

1. HTML β†’ DOM

The browser reads HTML top to bottom and builds the Document Object Model, a tree of nodes that JavaScript can read and change.

<body>
  <header><h1>Shop</h1></header>
  <main><p>Hello <b>Ana</b></p></main>
</body>
Drawing diagram…

2. CSS β†’ CSSOM

CSS is parsed into its own tree. CSS is render-blocking: the browser won't paint anything until it has the CSS, to avoid showing unstyled content that then jumps around.

3. Render tree

DOM + CSSOM combined, but only visible elements. display: none elements, <head> and <script> are left out. (visibility: hidden elements are included: they take space but are invisible.)

4. Layout (reflow)

The browser calculates the exact position and size of every box, based on the viewport width, fonts, padding and so on.

5. Paint

Fill in the pixels: text, colours, borders, shadows, images.

6. Composite

Some elements get their own layers (for example, elements with transform or opacity animations, or will-change). The GPU stacks these layers into the final image.

JavaScript can block everything

When the parser meets a normal <script>, it stops building the DOM, downloads and runs the script, then continues.

Drawing diagram…
AttributeDownloadsRunsOrder kept?Use for
(none)Blocks parsingImmediatelyβœ…Avoid in <head>
deferIn parallelAfter HTML is parsedβœ…Your app scripts βœ…
asyncIn parallelAs soon as downloaded❌Independent scripts (analytics)
type="module"In parallelDeferred by defaultβœ…Modern ES modules
<script src="/app.js" defer></script>
<script src="https://analytics.example.com/a.js" async></script>

Reflow and repaint: why some changes are expensive

After the first render, every change to the page may redo part of the pipeline:

You change…Pipeline steps redoneCost
width, height, top, margin, font size, adding elementsLayout β†’ Paint β†’ CompositeπŸ”΄ Expensive
color, background, box-shadow, visibilityPaint β†’ Composite🟑 Medium
transform, opacityComposite only🟒 Cheap

πŸ’‘ Animate only transform and opacity. Moving a box with transform: translateX(100px) runs smoothly on the GPU; moving it with left: 100px forces layout on every frame.

/* ❌ Janky: triggers layout 60 times per second */
.menu { transition: left 0.3s; }
.menu.open { left: 0; }

/* βœ… Smooth: compositor only */
.menu { transition: transform 0.3s; transform: translateX(-100%); }
.menu.open { transform: translateX(0); }

Layout thrashing

Reading a layout value (offsetHeight, getBoundingClientRect()) right after writing styles forces the browser to calculate layout immediately. In a loop, that's a disaster.

// ❌ Read-write-read-write: forces layout on every iteration
for (const box of boxes) {
  box.style.width = container.offsetWidth / 2 + "px";
}

// βœ… Read once, then write
const half = container.offsetWidth / 2;
for (const box of boxes) box.style.width = half + "px";

60 frames per second

To feel smooth, the browser draws a new frame every 16.7 ms (60 fps). Your JavaScript, style calculations, layout and paint must all fit in that budget.

Drawing diagram…

A long task (for example, 200 ms of JavaScript) freezes the page: clicks don't respond and animations stutter. That's what the INP metric measures (see Frontend Performance).

Key takeaways

  • HTML β†’ DOM, CSS β†’ CSSOM, combined into a render tree, then layout, paint, composite.
  • CSS blocks rendering; plain scripts block parsing. Use defer or async.
  • Changing geometry triggers expensive layout; transform and opacity are cheap.
  • Batch DOM reads and writes to avoid layout thrashing.
  • You have ~16 ms per frame for smooth 60 fps.