The idea: from bytes to pixels

A browser gets a page as text: HTML and CSS. To show it, the main thread parses the HTML into the DOM (a tree of nodes) and the CSS into the CSSOM (the rules). Then every frame runs the same steps. Style works out each element's final CSS. Layout works out where each box goes and how big it is. Paint records how each box looks and turns it into pixels, one layer at a time. Composite stacks the layers into the picture on screen; the compositor thread does this part.

HTML is parsed into the DOM and CSS into the CSSOM. Style combines them into the render tree. Layout gives each box x, y, width and height. Paint fills each layer with pixels. Composite stacks the layers on screen. Style, layout and paint run on the main thread; composite runs on the compositor thread.
Two trees in, one render tree, then layout, paint and composite: the first three on the main thread, the last on the compositor.

The page on the canvas loads a tiny document: an h1, a paragraph, a script, a hidden ad, a card and a footer, one stylesheet and one script. Every step costs a few milliseconds of a made-up but fixed clock, so you can compare the runs.

DOM, CSSOM and the render tree

The parser reads the HTML top to bottom and adds one DOM node per tag. When it meets <link rel="stylesheet"> it asks the network for the CSS and keeps going. The CSS file is parsed into the CSSOM once it has fully arrived; a half-read stylesheet is useless, because a later rule can override an earlier one.

Style then walks the DOM and matches the CSSOM rules to each element. The result is the render tree: only the boxes that will be drawn. <head>, <link> and <script> are not drawn, and neither is anything with display: none (here the ad). An element with visibility: hidden would still be there: it takes up space, it is just not painted.

Layout, paint and layers

Layout (Firefox calls it reflow) goes through the render tree and gives each box its x, y, width and height. Boxes in normal flow stack top to bottom, so a box that gets taller pushes every box after it down. Paint does not draw on the screen. It records drawing commands ("fill this rectangle, draw this text") and rasterizes them into the pixels of a layer.

Most of a page lives in one root layer. Some elements get a layer of their own: here the card, because of will-change: transform. Videos, canvases, fixed elements and running transform or opacity animations often get one too. The compositor can then move, scale or fade that layer without the main thread painting anything again.

Render-blocking CSS, parser-blocking JS

CSS blocks rendering. Without the CSSOM the browser does not know what anything looks like, so it draws nothing until the stylesheet has arrived and been parsed. The alternative would be a flash of unstyled content.

A plain script blocks the parser. A script can call document.write or read the DOM it sits in, so the parser stops at <script src> until the file has downloaded and run. The script itself waits for pending stylesheets, because it may ask for computed styles. Everything below the script waits too (Demo 1: the card shows up only at 362 ms).

Three timelines for the same page. Sync: parsing stops at the script from 70 to 300 ms, first paint at 171 ms shows only the heading and paragraph, the card appears at 362 ms. Defer: parsing ends at 100 ms, the whole page paints at 182 ms, the script runs at 270 ms, DOMContentLoaded at 300 ms. Async: the same paint at 182 ms, DOMContentLoaded at 100 ms, the script runs when it arrives at 270 ms.
Same page, same downloads: only the script attribute decides whether the card waits for app.js.
scriptdownloadsrunson the canvas
<script src>parser stops and waitsat once, then parsing goes onfirst paint 171 ms (half the page), card 362 ms
deferin parallelafter parsing, in order, before DOMContentLoadedfirst paint 182 ms (whole page), DCL 300 ms
asyncin parallelas soon as it arrives, in any orderfirst paint 182 ms (whole page), DCL 100 ms

So keep CSS small and early, put scripts at the end of <body> or mark them defer, and use async for scripts that do not touch the page, such as analytics (Demos 1–3).

Reflow vs repaint vs composite only

After the load, every change from JavaScript or CSS runs a frame, but not every frame runs every stage. A change to geometry (width, font-size, adding a node) needs layout again: a reflow, then paint and composite. A change that only alters looks (color, background) skips layout: a repaint. A change to transform or opacity on an element with its own layer skips both: the compositor just draws the layer somewhere else or more see-through.

Three rows of the pipeline. font-size: style, layout, paint and composite all run, 25 ms. color: style, paint and composite run, layout is skipped, 13 ms. transform or opacity: style and composite run, layout and paint are skipped, 3 ms.
The later in the pipeline a change starts, the less work the frame does.
changestagesframe on the canvas
h1 font-size: 48pxstyle, layout, paint, composite25 ms ✗ over 16.7 ms (Demo 4)
.intro color: redstyle, paint, composite13 ms ✓ (Demo 5)
.box transform / opacitystyle, composite3 ms ✓ (Demo 6)

At 60 Hz a frame has 16.7 ms. A frame that takes longer drops: the animation stutters. That is why smooth animations use transform and opacity, not top, left or width. One more trap: reading offsetHeight or getBoundingClientRect() right after a write forces layout to run at once, inside your script. Doing that in a loop (write, read, write, read) runs layout many times in one frame; this is called layout thrashing. Batch the reads, then the writes.

See also the browser event loop: where in a turn of the loop the rendering steps run, and why a long task delays the next frame. Virtual DOM diffing is how React keeps the number of DOM changes, and so the reflows, small. HTTP caching decides whether style.css and app.js need the network at all, and server-side vs client-side rendering decides whether the first HTML already holds the content.

What the page leaves out

The preload scanner (real browsers find style.css and app.js early and fetch them while the parser is blocked), incremental and streamed parsing, rendering in the middle of a long parse, fonts and images (which also block or shift the layout), the layer tree and paint invalidation at finer grain than a whole layer, tiling and GPU rasterization, the commit from the main thread to the compositor, requestAnimationFrame and the vsync clock, scrolling on the compositor, content-visibility and contain, and the real costs, which depend on the page size and the device.