# How the browser renders a page

> Parse, style, layout, paint and composite — and which CSS changes trigger which steps.

Concept · The browser · https://levelcode.org/concepts/critical-rendering-path

Turning HTML and CSS into pixels is a pipeline. Knowing its stages tells you why a page is slow to appear, and why an animation stutters.

```text
HTML → DOM ┐
           ├→ Style → Layout → Paint → Composite
CSS → CSSOM┘
```

1. **Parse.** HTML becomes the DOM and CSS becomes the CSSOM. A plain `<script>` *blocks* HTML parsing until it has downloaded and run. CSS *blocks rendering*, because the browser won't paint until it knows the styles.
2. **Style.** Match selectors to elements to get each element's computed style.
3. **Layout** (also called reflow). Compute every box's size and position. This is geometry, and it can cascade: a wider sidebar changes the width of everything beside it.
4. **Paint.** Fill in pixels for text, colours, borders and shadows, in layers.
5. **Composite.** The GPU stacks the layers into the final frame.

## First render: the critical path

Everything the first paint waits for is on the **critical rendering path**. To shorten it:

- Put `defer` (or `type="module"`) on scripts so they don't block parsing.
- Keep the CSS needed above the fold small. Load the rest without blocking.
- Make the hero image discoverable in the HTML, and give it `fetchpriority="high"` instead of lazy-loading it.

Getting these right is most of improving [LCP](https://levelcode.org/concepts/core-web-vitals).

## Updates: which steps run again

| You change | Steps that re-run |
|---|---|
| `width`, `height`, `top`, `margin`, font size, adding DOM nodes | Layout → Paint → Composite |
| `color`, `background`, `box-shadow` | Paint → Composite |
| `transform`, `opacity` | Composite only (on a compositor layer) |

That's why smooth animations use `transform` and `opacity`. The GPU can run them at 60 fps even while the main thread is busy.

## Layout thrashing

Reading a layout property (`offsetHeight`, `getBoundingClientRect()`) right after a write forces the browser to run layout **synchronously**. Doing that in a loop runs layout once per element:

```js
// Slow: write, read, write, read… forces layout every iteration
for (const el of items) el.style.width = el.parentElement.offsetWidth / 2 + 'px'

// Fast: read everything, then write everything
const widths = items.map((el) => el.parentElement.offsetWidth)
items.forEach((el, i) => (el.style.width = widths[i] / 2 + 'px'))
```

All of this happens on the main thread, in the rendering step of [the event loop](https://levelcode.org/concepts/event-loop).
