How browsers render contentFrom bytes to pixels

100%

From bytes to pixels.

A web page reaches the browser as a stream of bytes and leaves it as a grid of coloured pixels, redrawn up to sixty or more times a second while something on it moves. In between sits an assembly line: parse, style, lay out, paint, rasterize, composite. Each station hands the next a different data structure, and each one can be skipped when nothing it depends on has changed. Once you can name the stations and say which of them a change wakes up, most front-end performance advice stops being a list of rules and starts being common sense.

Beginner33 minUpdated 2 Oct 2026

The idea.

Rendering is not one step. It is a chain of conversions, each producing the input of the next, and the browser reruns only the links a change actually touches.

Open Copperpot's product page for the Enamel casserole on a phone. The server sends text: 96 KB of markup and, a little later, a 48 KB stylesheet. The screen needs something completely different, a rectangle of pixels with a colour for each one. No single algorithm goes straight from one to the other. The browser gets there in stages, and each stage builds a structure that answers one question.

Parsing asks what the document contains, and answers with the DOM. Parsing the stylesheet asks which rules exist, and answers with the CSSOM. Style asks which of those rules win for each element, and answers with a computed style per element. The layout tree asks which elements actually produce boxes. Layout asks where each box goes and how big it is. Paint asks what to draw and in which order, and answers with a list of drawing commands. Raster runs those commands to get pixels, and compositing stacks the rasterized layers into the frame you see.

The split is what makes the web fast enough. Because every stage keeps its output, a later change only has to redo the stages whose inputs it changed. Recolouring a button border does not move anything, so layout is skipped. Sliding a photo with a transform does not even need repainting. Knowing which stage a change wakes up is the single most useful thing this topic teaches.

The pipeline, first load

Bytesnetwork
decode, tokenize, build
DOM (and CSSOM, from the stylesheet bytes)main thread
match selectors, cascade
Computed stylesmain thread
keep only elements that make boxes
Layout treemain thread
compute sizes and positions
Geometry (fragments)main thread
record drawing commands in paint order
Display listsmain thread
commit to the compositor
Tiles of pixelsraster threads
stack layers, draw
Frame on screencompositor and GPU
The stations of the pipeline from the first byte to the first frame. The five in the middle run on the renderer's main thread; raster and composite run on other threads, which is why a busy main thread can still scroll in some cases.

What each stage produces for Copperpot's page

StageInputOutputOn Copperpot's product page
Parse HTMLBytes of markupDOMAbout 1,200 element nodes plus their text nodes
Parse CSSBytes of stylesheetCSSOMAbout 1,900 rules from main.css, plus the browser's own defaults
StyleDOM + CSSOMComputed style per elementEvery element gets a value for every property, inherited or not
Layout treeDOM + computed stylesTree of boxesHidden size guide dropped; ::before badges added
LayoutLayout tree + viewport widthSizes and positionsRecomputed if the phone is rotated: 393 px wide becomes 852 px
PaintGeometry + stylesDisplay listsCommands such as fill rectangle, draw text run, draw image
RasterDisplay listsPixels in tilesImage decode of the hero photo happens here
CompositeRasterized layersA frameHeader, page and toast stacked in the right order
Was this section helpful?

How it works.

The same Copperpot page, one station at a time, with what each one reads, what it builds and what it refuses to do until its inputs are ready.

1

Bytes to characters to tokens

Before anything can be parsed, bytes have to become characters, and that needs an encoding. The browser looks for a byte order mark at the very start, then for a charset on the Content-Type header, and failing both it scans the beginning of the file for a meta charset declaration. The HTML Standard encourages browsers to look only at the first 1,024 bytes for it, so Copperpot puts <meta charset="utf-8"> as the first element in head. Declared later, or not at all, a page can be decoded with a guessed encoding and, when the guess turns out wrong, decoded again from the start.

The tokenizer then reads characters and emits tokens: a doctype, start tags with their attributes, end tags, runs of text, comments. It is a state machine that changes mode as it goes, so the same less-than sign means a tag in body text, plain text inside a <textarea> and nothing special inside a <script> until the matching end tag. Tokens are handed to tree construction one at a time; the browser does not wait for the whole file.

The top of Copperpot's product page, and the tree it becomes

<!doctype html>
<meta charset="utf-8">
<title>Enamel casserole, 24 cm · Copperpot</title>
<link rel="stylesheet" href="/css/main.css">
<header class="site-bar">
  <a href="/" class="logo">Copperpot</a>
  <button class="basket" aria-label="Basket, 0 items"></button>
</header>
<main class="product">
  <h1>Enamel casserole, 24 cm</h1>
  <p class="price">£64
  <p class="stock">In stock, ships Monday
  <table class="specs">
    <tr><td>Capacity<td>4.2 litres
#document
├─ <!DOCTYPE html>
└─ html                       ← never written, inserted
   ├─ head                    ← never written, inserted
   │  ├─ meta charset=utf-8
   │  ├─ title  "Enamel casserole, 24 cm · Copperpot"
   │  └─ link rel=stylesheet
   └─ body                    ← inserted at the first <header>
      ├─ header.site-bar …
      └─ main.product
         ├─ h1  "Enamel casserole, 24 cm"
         ├─ p.price  "£64"     ← closed when the next <p> opened
         ├─ p.stock  "In stock, ships Monday"
         └─ table.specs
            └─ tbody          ← never written, inserted
               └─ tr
                  ├─ td "Capacity"
                  └─ td "4.2 litres"
2

Tokens to the DOM

Tree construction keeps a stack of open elements and decides, token by token, where each node goes. It knows the rules a person writing the markup may have skipped: html, head and body exist even when nobody wrote them, a table row always sits inside a tbody, and a new <p> closes an open one. Copperpot's template omits all of those and the tree still comes out the same in every browser, because the standard defines a recovery for every kind of mistake. There is no such thing as a fatal error when parsing HTML, which is very different from parsing JSON or XML.

Nodes are appended as soon as their tokens arrive, so the DOM grows while the bytes are still downloading. That is what lets a browser show the header and the title of a long page before its footer exists. Two things can interrupt the flow: a classic script without async or defer makes the parser stop and run it, because the script could write more markup into the stream, and the first paint waits for the stylesheets in head. Who waits for whom, and how async, defer and the preload scanner change it, is the subject of What blocks the parser and the first paint.

Markup Copperpot's template gets wrong, and what the parser does

WrittenParser's recoveryVisible effect
<p class="price">£64 then <p class="stock">The second start tag closes the open paragraph firstTwo sibling paragraphs, as intended
<table><tr>Inserts a tbody element around the rowA selector table > tr matches nothing; table > tbody > tr does
<p>See <div>size guide</div></p>A div cannot sit inside a p, so the p is closed before it and the stray </p> becomes an empty paragraphAn extra empty p after the div, with its margins
<b>Sale <i>today</b> only</i>Closes and reopens the formatting elements so the tree stays a tree"only" stays italic but not bold
No <html>, <head> or <body>Inserts all three at the right momentsNone; document.body still works
3

The CSSOM and style

main.css goes through its own parser and becomes the CSSOM: style sheets holding rules, each rule a selector and a block of declarations. It is a separate structure from the DOM, and on its own it says nothing about any particular element.

Style calculation joins the two. For each element the engine finds the rules whose selectors match it, sorts the competing declarations by the cascade (origin and importance, then layers, then specificity, then order of appearance), fills in what nothing set by inheriting from the parent or using the initial value, and resolves relative values where it can, such as 1.5em to 24px. The result is a computed style for every element and every property. Copperpot's price paragraph ends up with a colour, a font size and a margin even though its own rule only names two of those.

Engines avoid repeating work here: they index rules so that most selectors are never tried against most elements, share computed styles between siblings that match the same rules, and after a change restyle only the elements a changed class or attribute could affect. Style is still proportional to how many elements need it, which is why a class toggled on body can be more expensive than the same class on one button.

Where p.price's computed values come from

PropertyComputed valueCame from
font-size24px.product .price { font-size: 1.5rem } with a 16px root
colorrgb(122, 44, 20).price { color: var(--rust) }, the custom property resolved
font-family"Fraunces", serifInherited from main.product, which inherited it from body
margin-top24pxp { margin-block: 1em } in the browser's own stylesheet; em uses the element's own font size, so 1em is 24px here
displayblockThe browser's default for p
4

From elements to boxes

Layout does not work on the DOM. It works on a tree of boxes built from the DOM and the computed styles, often called the layout tree or render tree. The two trees disagree in instructive places. Elements whose display is none produce no box at all, and neither do their descendants; head and everything in it is display:none by default. Pseudo-elements such as ::before produce boxes even though they are not in the DOM. An element with display:contents gives up its own box but its children keep theirs, as if they belonged to its parent. And an element with visibility:hidden keeps its box, because it still takes up space; it is simply not drawn.

Part of Copperpot's product block, as nodes and as boxes

Part of Copperpot's product block, as nodes and as boxes
Part of Copperpot's product block, as nodes and as boxesParts: main.product, span.badge, div.size-guide, div.swatch-row, button Teal, p.sold-out-note, block box (main), ::before "New", inline box (span.badge), box (button Teal), box (p.sold-out-note).

Layout treewhat layout works on

DOMwhat the parser built

one box

one box

pseudo-element

one box, parent skipped

box kept

main.product

span.badge
CSS adds ::before with content "New"

div.size-guide
display none until opened: no box

div.swatch-row
display contents: no box of its own

button Teal

p.sold-out-note
visibility hidden while in stock

block box (main)

::before "New"
a box with no DOM node

inline box (span.badge)

box (button Teal)
laid out as a child of main

box (p.sold-out-note)
takes space
not painted

Four ways to hide Copperpot's size guide, and what each leaves in the pipeline

CSSIn the layout tree?Takes space?Painted?Screen readers
display: noneNoNoNoRemoved, with all its descendants
visibility: hiddenYesYesNoNot announced
opacity: 0YesYesYes, fully transparentStill announced, and still clickable
content-visibility: hiddenIts own box only; the contents are skippedIts own size (set it with contain-intrinsic-size)Contents noContents hidden; their rendering state is kept for a quick reveal
5

Layout

Layout walks the box tree and works out the size and position of every box. Widths mostly flow down from the parent: the viewport is 393 CSS pixels wide on Copperpot's test phone, main has 16px of padding on each side, so the product block's content is 361px wide. Heights mostly flow up from the children: the price paragraph is as tall as its line of text, which depends on the font, which may not have loaded yet. Text is shaped and broken into lines here, so a longer product name can push everything below it down. The rules that decide where each box goes (normal flow, flex, grid, positioned boxes) belong to the formatting contexts topics; what matters here is that layout's output is geometry, and that geometry is what paint and hit testing read.

Because a box's position depends on what comes before it and its size on what is inside it, a change in one place can move many others. Engines mark dirty boxes and skip clean subtrees, and in Chromium the result of layout is an immutable fragment tree that is reused where its inputs did not change. The first layout of a page is usually just called layout; later ones are often called reflows.

6

Paint, then raster

Paint does not produce pixels. It walks the laid-out boxes in paint order and records commands: fill this rounded rectangle with cream, draw this text run in rust at these coordinates, draw this image into that rectangle. Paint order is not DOM order. Backgrounds go under borders, borders under content, and positioned or z-indexed boxes go where their stacking context puts them, so Copperpot's sticky header is recorded after the product photo it overlaps even though the header comes first in the markup.

Raster executes those commands into actual pixels. Large areas are cut into tiles and drawn in parallel on raster threads, often with the GPU's help, starting with the tiles in or near the viewport. Images are decoded here too, which is why a large hero photo can sit in memory as compressed bytes well before it is ready to draw. Keeping raster off the main thread means a slow decode does not freeze script or input.

7

Composite

Parts of a page may be painted into separate layers: a video, a canvas, a fixed header, an element being animated with transform or opacity. Compositing stacks those rasterized layers with their current offsets, transforms, clips and opacities, and the result is the frame sent to the display. Because the layers are already pixels, moving one or fading it means recompositing, not repainting. That is how a page can scroll, or a photo can slide, while the main thread is busy elsewhere. Which elements get a layer of their own, and what that costs in memory, is covered in Layers and compositing.

The clock the pipeline races

Frame at 60 Hz
16.7 ms
time between screen refreshes; faster displays leave less
Left for your work
about 10 ms
web.dev's rule of thumb once the browser's own work is counted
Sharing the main thread
script + 4 stages
HTML parsing, style, layout and paint wait while your JavaScript runs
Off the main thread
2 stages
raster and composite, plus scrolling in Chromium when nothing blocks it
8

The whole first load, on one clock

Copperpot's product page from the first byte to the hero photo

Scenario 1 of 2: As described.

Timeline as a list

Copperpot's product page from the first byte to the hero photo: 6 lanes, from 0 ms to 360 ms.

  1. 0–8 ms · Network · HTML chunk 1
  2. 2–28 ms · Main: parse HTML · head, header, title, price
  3. 4–180 ms · Network · main.css in flight
  4. 4 ms · Main: parse HTML · finds main.css
  5. 8–300 ms · Network · hero photo in flight
  6. 28–180 ms · Screen, Main: style, layout, paint · window: DOM ahead of the CSSOM
  7. 58–66 ms · Network · chunk 2
  8. 66–84 ms · Main: parse HTML · gallery, options
  9. 108–114 ms · Network · chunk 3
  10. 114–126 ms · Main: parse HTML · reviews, footer
  11. 126 ms · Main: parse HTML · DOM done: interactive (ok)
  12. 180–186 ms · Main: style, layout, paint · CSSOM
  13. 180 ms · Network · main.css arrives
  14. 186–200 ms · Main: style, layout, paint · style
  15. 200–214 ms · Main: style, layout, paint · layout
  16. 214–221 ms · Main: style, layout, paint · paint
  17. 221–224 ms · Compositor thread · commit
  18. 224–232 ms · Raster and decode · raster tiles
  19. 233.3 ms · Screen · FP and FCP: first frame (ok, ok)
  20. 300–303 ms · Main: style, layout, paint · paint image
  21. 303–318 ms · Raster and decode · decode + raster hero
  22. 333.3 ms · Screen · hero shown (ok)
Illustrative timings on a mid-range phone on a fast connection, with time 0 at the first byte of HTML. The page is simplified: its scripts and web font, which What blocks the parser and the first paint walks through, are left out so the stages stand out. The parser builds the DOM chunk by chunk and has finished at 126 ms, but nothing can be drawn until main.css arrives at 180 ms. Style, layout and paint then run once over the whole page and the first frame lands at about 233 ms. The second tab shows the same visit with main.css already in the cache.

Two things to read off the waterfall. First, parsing is not the bottleneck on this page: the DOM is complete 54 ms before the stylesheet arrives, and the screen stays blank for that whole window. Second, rendering is incremental when its inputs allow it. With the CSSOM ready early, the browser paints the part of the DOM it has and adds to it as chunks arrive, so the visitor sees the price a sixth of a second sooner. Two milestones from the Paint Timing specification name these moments: first paint is the first frame that shows anything other than the default background, and first contentful paint is the first frame that shows text, an image, a non-blank canvas or an SVG from the DOM. On this page the first frame already holds the product name, so the two coincide; a page whose first frame is only a coloured header bar would record them apart. Why the stylesheet holds back the first frame, and what preloading and inlining do about it, is the subject of What blocks the parser and the first paint, which also follows readyState, DOMContentLoaded and load through the same visit; the page-load card turns the same picture into LCP.

Who hands what to whom for the first frame (Chromium)

Who hands what to whom for the first frame (Chromium), as an ordered list of steps:
Who hands what to whom for the first frame (Chromium)10 steps between Network service, Renderer main thread, Compositor thread, Raster and decode threads, GPU process (Viz). The steps are listed as text after the diagram.GPU process (Viz)Raster and decode threadsCompositor threadRenderer main threadNetwork servicedecode, tokenize, append DOM nodesCSSOM, style, layout, pre-paint, paintlayerize, cut layers into tilesaggregate with other frames on screen, drawHTML bytes, chunk by chunk1main.css bytes2commit: display lists + property trees3raster the tiles near the viewport first4tiles ready in GPU memory5compositor frame: which tile goes where6
  1. Network service → Renderer main thread: HTML bytes, chunk by chunk
  2. Note over Renderer main thread: decode, tokenize, append DOM nodes
  3. Network service → Renderer main thread: main.css bytes
  4. Note over Renderer main thread: CSSOM, style, layout, pre-paint, paint
  5. Renderer main thread → Compositor thread: commit: display lists + property trees
  6. Note over Compositor thread: layerize, cut layers into tiles
  7. Compositor thread → Raster and decode threads: raster the tiles near the viewport first
  8. Raster and decode threads → Compositor thread (reply): tiles ready in GPU memory
  9. Compositor thread → GPU process (Viz): compositor frame: which tile goes where
  10. Note over GPU process (Viz): aggregate with other frames on screen, draw
Was this section helpful?

In practice.

After the first frame the pipeline keeps running, but rarely in full. Each thing a Copperpot customer does wakes up a different part of it, and a good front-end design says which part on purpose. When those reruns happen (at most once per frame, between tasks) is the subject of The frame and the event loop.

One visit to the casserole page, stage by stage

Everything ran once: parse, style, layout, paint, raster, composite. The photo slot is laid out at its final size from the width and height attributes, so when the decoded photo arrives a frame later nothing below it moves.

First frame. Store “Copperpot”, showing the product view. Cart button “Basket” with badge 0 (its accessible name says “Cart, 0 items”, not the number alone). Product (showing) “Enamel casserole” by “Cast iron, oven safe to 260 °C”: rating 4.8 (“312 reviews”); price £64.00. Gallery: image 1 of 5 (“Cream casserole, lid on, side view”); thumbnails beside it. Colour (swatches, a radio group): Cream (selected), Teal, Rust. Size (tiles, a radio group): 24 cm (selected), 28 cm. Stock: “In stock”. Quantity 1 (stepper “Quantity”). Buttons: “Add to basket”. Delivery: “Free delivery over £75”. Folded sections: Details, Reviews, Care and cleaning. Note on gallery: box sized before the image decodes

  1. Its own layer while swiping3Compositor thread
  2. Text change means layout2Renderer main thread
  3. Boxes created only when opened2Renderer main thread

Which stages each interaction runs

Pipeline
  1. JavaScript, width 1, Runs
  2. Style, width 1, Runs
  3. Layout, width 1, Runs
  4. Paint, width 1, Runs
  5. Raster, width 1, Runs off the main thread
  6. Composite, width 1, Runs off the main thread
  • Runs
  • Skipped (output reused)
  • Runs off the main thread
Start

As it starts. 6 steps follow.

One row of pipeline stages; step through Copperpot's interactions. Stages that are skipped keep their previous output, which is the whole reason the pipeline is split. Exact behaviour varies by engine; this is the shape web.dev describes and Chromium follows.

Main-thread work behind Copperpot's first frame

Main-thread work behind Copperpot's first frameParsing the HTML is the largest single piece of main-thread work, but it overlaps the download, so the frame still waits on the stylesheet more than on any stage.010 ms20 ms30 ms40 ms50 ms60 msParse HTMLParse CSSStyleLayoutPaintRaster (off main)56 ms6 ms14 ms14 ms7 ms8 msStageTime (ms)Main-thread work behind Copperpot's first frameParsing the HTML is the largest single piece of main-thread work, but it overlaps the download, so the frame still waits on the stylesheet more than on any stage.020 ms40 ms60 msParse HTMLParse CSSStyleLayoutPaintRaster (off main)56 ms6 ms14 ms14 ms7 ms8 msStageTime (ms)
Illustrative milliseconds on a mid-range phone, from the first-load waterfall above. Parsing happened in three bursts while the chunks arrived; the other stages ran back to back after main.css landed. Raster ran on its own threads.
Data
Stagems
Parse HTML56
Parse CSS6
Style14
Layout14
Paint7
Raster (off main)8

The same interactions in Copperpot's code, with the route each one takes

/* style → paint → composite: colour never moves a box */
.swatch:hover { border-color: var(--rust); }

/* composite only, once the track has its own layer */
.gallery-track {
  transition: transform 220ms ease-out;
}

/* the photo's box is reserved before the bytes arrive */
.gallery img { aspect-ratio: 1; width: 100%; height: auto; }

/* closed by default: no boxes, no layout cost */
.reviews[hidden] { display: none; }
// style → layout → paint → composite: text can change size
sizeTiles.addEventListener('change', (e) => {
  price.textContent = prices[e.target.value];
  stock.textContent = stockLine(e.target.value);
});

// composite only: no geometry is read or written
function showPhoto(i) {
  track.style.transform = `translateX(${-i * 100}%)`;
}

// the expensive one: 40 cards join the layout tree
reviewsToggle.addEventListener('click', () => {
  reviews.hidden = !reviews.hidden;
});

Seeing the stages for yourself

Every major browser's developer tools can record a trace of the main thread. In Chrome's Performance panel the stages in this topic appear under their own names, such as Parse HTML, Recalculate style, Layout and Paint, with the commit and the compositor's work on other tracks. Record Copperpot's size change and you will see a short recalculate style, a layout and a paint; record the gallery swipe and the main thread should be nearly empty while frames keep arriving. Paint flashing in Chrome's Rendering tab colours each repainted area green, which turns the hover and the swipe into an instant visual check of which route a change took.

A trimmed, annotated main-thread trace of Copperpot's first load and one size change

2 ms     Parse HTML            chunk 1 → head, header, title, price
66 ms    Parse HTML            chunk 2 → gallery, options
114 ms   Parse HTML            chunk 3 → reviews, footer; DOM complete
         (idle: nothing to style until main.css arrives)
180 ms   Parse stylesheet      main.css → CSSOM
186 ms   Recalculate style     ~1,200 elements get computed styles
200 ms   Layout                every box gets a size and position
214 ms   Paint                 display lists recorded, not pixels
221 ms   Commit                handed to the compositor thread
         (raster tiles and the frame happen on other tracks)
Event: change          handler sets two textContent values
Recalculate style      two paragraphs
Layout                 the price, stock line and what follows
Paint                  the changed area
Commit

Where this shows up in system designs

Product detail pages
The first frame waits on the stylesheet and the hero image's box; picking a variant should change text and a price, never rebuild the page. Reserving the image's size keeps later frames from moving content (Amazon product detail page).
Listing pages with photo galleries
Swiping photos and showing a sticky booking card are composite-only when built on transform; a calendar that opens below the card is a layout change for everything after it (Airbnb listing page).
News feeds
Every appended post is new DOM, style and layout. Off-screen posts can skip layout and paint, and scrolling stays on the compositor as long as no listener blocks it (Facebook news feed).
Canvas-based editors
A design tool that draws its board into a canvas skips the DOM, style and layout for the shapes and owns its own paint, which trades the browser's pipeline for a renderer of its own (Figma infinite canvas).

Saying it in an interview

Q1
Warm-up"What happens between the browser receiving HTML and the user seeing the page?" What is a strong two-minute answer?
Name the stages and what each produces. The bytes are decoded and tokenized, and tree construction builds the DOM incrementally as chunks arrive. CSS is parsed into the CSSOM. Style matches rules to elements and computes a value for every property. The layout tree keeps only elements that produce boxes, so display:none is gone and pseudo-elements are added. Layout computes geometry, paint records drawing commands in stacking order, raster turns those into pixels in tiles off the main thread, and the compositor stacks layers into a frame. Then say what blocks it: stylesheets hold the first render, classic scripts pause the parser.
Q2
DesignIn a product page design, where does this knowledge change a decision?
Three places. Give images and embeds their dimensions so later frames do not move content. Make interactions take the cheapest route on purpose: hover and selection states change colour, galleries and drawers move with transform, and only real content changes touch layout. And keep heavy, rarely opened sections such as reviews out of the first layout, either with display:none until opened or content-visibility:auto, so the first frame does less work.
Q3
Follow-upThe interviewer asks why animating top is worse than animating transform. What do you say?
top is a geometry property, so each frame of the animation reruns layout and paint on the main thread, and any script running at the same time makes it stutter. transform on an element with its own layer is applied by the compositor to pixels it already has: no layout, no paint. Mention that the layer costs memory, which is the trade-off covered under layers and compositing.
Q4
Follow-up"Is the DOM what the user sees?"
No. The DOM is the document's content; what the user sees is derived from it plus the CSS. An element can be in the DOM with no box (display:none), a box can exist with no DOM node (::before), and paint order can differ from DOM order. That is why accessibility and visual order can drift apart, and why hidden content can still cost parse and style time.
Was this section helpful?

Trade-offs.

The pipeline's design is a set of trade-offs: when to show a first frame, and what to keep out of the layout tree. Each choice buys speed with something else: a blank screen, a jump, a gap or an accessibility trap. Skipping layout and paint for long off-screen sections with containment and content-visibility is covered in Rendering less.

01
What a browser should put on screen before Copperpot's page has fully arrived
Chosen:Paint as soon as the head's stylesheets are ready, without waiting for images
  • Pro:The first frame is styled, so nothing jumps from unstyled to styled
  • Pro:Text and layout appear while the hero photo is still downloading, in a box already sized for it
  • Pro:Later chunks of HTML are painted as they arrive, so long pages fill in from the top
Downside we accept:
  • Con:A slow stylesheet keeps the whole screen blank, even when the DOM is complete
  • Con:Images pop in later, and without reserved dimensions they push content down
Ruled out:Paint the DOM at once and restyle when the CSS lands

A flash of unstyled content: raw HTML in default styles, then a jump to the real design; Layout and paint run twice for the same content, and anything the visitor was about to tap moves

Ruled out:Wait for every sub-resource, images included, then paint once

The screen stays blank until the slowest image or frame finishes, 300 ms instead of 233 ms on this simplified page and far worse on a slow network; Text the visitor could already read is held back behind pixels they may not scroll to

02
How the closed size guide is hidden
Chosen:The hidden attribute (display:none) until opened
  • Pro:No boxes, so it costs nothing in layout or paint while closed
  • Pro:Removed from the accessibility tree and tab order, matching what is on screen
Downside we accept:
  • Con:Opening it creates its boxes and lays out everything after it
  • Con:Its size cannot be measured while closed
Ruled out:visibility: hidden

Leaves a blank gap the size of the guide on the page; Still laid out on every layout

Ruled out:opacity: 0

Still painted, still clickable and still read out by screen readers; Invisible controls can trap keyboard users

Things people get wrong about the pipeline

Q1
Waiting"The browser downloads the whole HTML file, then renders it."
Parsing is incremental and so is rendering. The DOM grows chunk by chunk, and once the stylesheets in head are ready the browser can paint the part it has. That is why sending the top of the page early helps, and why the order of markup matters.
Q2
Hidden"Elements with display:none cost nothing."
They cost nothing in layout and paint, but they were still downloaded, parsed into DOM nodes and given computed styles, and they still count towards the size of the DOM. Five hidden copies of a menu for five breakpoints are five copies of parse and style work.
Q3
Paint"Paint means drawing pixels."
Paint records drawing commands; raster turns them into pixels on other threads, tile by tile. The distinction matters when reading a trace: a long paint on the main thread and a slow image decode on a raster thread are different problems with different fixes.
Q4
GPU"Putting transform: translateZ(0) on everything makes the page faster."
It gives each element its own layer, and each layer is a block of GPU memory that must be rasterized and composited. A few layers for things that move are a win; hundreds of them can make a phone slower or run it out of memory. Layer budgets are covered in Layers and compositing.
Q5
Order"What comes later in the HTML is drawn on top."
Paint order follows stacking contexts, positioning and z-index, not just source order, which is how Copperpot's header can be first in the markup and still paint over the photo when they overlap.
This topic
Decoding, tokenizing and tree construction, including error recovery
The CSSOM, style and computed values
The layout tree versus the DOM, layout, paint, raster and composite
Which stages a change reruns, and the threads behind the first frame
Elsewhere
Why stylesheets and scripts hold up parsing and the first paint, and readyState during a loadWhat blocks the parser and the first paint
When rendering runs relative to tasks and requestAnimationFrameThe frame and the event loop
Which elements get their own layer, and what layers costLayers and compositing
Interleaved reads and writes that force layoutForced layout and layout thrashing
How normal flow, flex, grid and positioning place boxesFormatting contexts and CSS positioning
Turning the first-load picture into LCP and CLSPage load time
Was this section helpful?
Builds on this
What blocks the parser and the first paint
Read next