How a Browser Turns Markup Into a Page

Parsing, the DOM, style matching, layout and paint. Knowing this pipeline explains why script placement matters, why images need dimensions, and why broken markup still renders.

Concept

Between the moment a file arrives and the moment a reader sees it, the browser runs a fixed pipeline. Understanding it is what turns a list of memorised rules - put scripts at the end, set width and height on images - into rules you can reason about.

A pipeline diagram: download the HTML bytes, parse them into a DOM tree, match stylesheets to produce computed styles, run layout to place every box, then paint. A side panel explains that a blocking script pauses the parser.
Download, parse, style, layout, paint - and the one thing that stops the parser mid way.

The five steps

1. Bytes to characters

The file arrives as bytes. Before anything can be read, the browser needs the character encoding, which it takes from the HTTP header or from <meta charset>. This is why the charset declaration belongs in the first bytes of the head: declare it late and the browser has already guessed, and a wrong guess turns every non ASCII character into a mangled symbol.

2. Characters to the DOM

The parser reads the character stream and builds the Document Object Model: a tree of nodes where every element is a node and every element inside it is a child. The DOM is what CSS matches against and what JavaScript manipulates. It is built as the file streams in, not after it finishes downloading, which is why a long page starts appearing before it has fully arrived.

<body>
  <h1>Title</h1>
  <p>Some <em>text</em>.</p>
</body>
body
├── h1
│   └── "Title"
└── p
    ├── "Some "
    ├── em
    │   └── "text"
    └── "."

3. Styles are matched

Stylesheets are parsed into their own model, then every node gets a fully computed style: the browser default, overridden by your stylesheet, overridden by inline styles. Nodes that end up invisible - display: none, and everything inside head - are dropped and never take part in layout.

4. Layout

The browser walks the styled tree and works out the geometry: how wide is each box, how tall, and where does its top left corner sit. This is the expensive step and it depends on the viewport, so it runs again on every resize and on anything that changes a size.

5. Paint

Finally the pixels: text, backgrounds, borders, shadows and images, drawn in the order the layers require.

Why a script in the head stops everything

A plain <script src> blocks the parser. The browser stops building the DOM, downloads the file, executes it, and only then continues. Everything below that line is unparsed and therefore unpainted, so the reader stares at a blank page for the duration.

<!-- blocks parsing right here -->
<script src="/scripts/app.js"></script>

<!-- downloads in parallel, runs after parsing, keeps document order -->
<script src="/scripts/app.js" defer></script>

<!-- downloads in parallel, runs the moment it arrives, order not guaranteed -->
<script src="/scripts/analytics.js" async></script>

The historical fix was to put scripts at the very end of the body. defer is better: the download starts early and runs in parallel with parsing, and execution still waits for a complete DOM.

Why images need width and height

When layout runs, an image whose dimensions are unknown is given no space. The text below it moves up to fill the gap. When the image finally arrives, layout runs again and everything jumps down - the reader loses their place, or taps the wrong thing. Stating the intrinsic size lets the browser reserve the correct box on the first pass.

<img src="/images/hall.jpg" alt="An empty lecture hall" width="1200" height="675">

The numbers describe the aspect ratio; CSS is still free to resize the image. This is the single cheapest fix for layout shift, which Google measures directly as Cumulative Layout Shift.

Why broken markup still renders

HTML has no fatal parse errors. A missing end tag, an overlapping pair, a stray bracket - the parser repairs all of it and carries on, following recovery rules in the specification. The repair is a guess, and the tree you get may not be the tree you wrote.

<p>First paragraph
<p>Second paragraph

Here the parser closes the first paragraph when it meets the second opening tag, and the result happens to be what you meant. Now try this:

<p>Total: <div>1,200</div></p>

A paragraph cannot contain a div, so the parser closes the paragraph before the div, leaves the div as a sibling, and discards the closing tag. Three nodes, none of them what you wrote. This is why it looks right is not the same as it is right, and why the Elements panel - which shows the DOM, not your file - is the tool that tells the truth.

Important rules

  • The DOM is built from the parsed result, not from your source. Always debug against the Elements panel.
  • Markup order is reading order, keyboard order, and paint order. Put the important content early in the body.
  • Stylesheets block painting; scripts block parsing. Both cost the reader time, in different ways.
  • Layout re runs on resize, on font load and on any size change. Cheap markup makes it cheap.

Common mistakes

  • Loading a large script in the head with no defer, then blaming the server for a slow first paint.
  • Omitting image dimensions and shipping a page that jumps while it loads.
  • Reading the page source rather than the Elements panel while debugging a nesting problem.
  • Declaring the encoding after a title that already contains a non ASCII character.
  • Hiding content with display: none and expecting it to be measurable. It is not in the layout at all.

Best practices

  • Charset first in the head, viewport second, stylesheet before the body content.
  • Every script gets defer unless there is a specific reason it cannot.
  • Every raster image gets width and height.
  • Put the main content of the page before the sidebar in the markup, and move it visually with CSS if the design calls for it.

Practice

  1. Write a page with a script in the head that loops for two seconds before the body content. Reload and describe what the reader sees. Add defer and describe the difference.
  2. Write <p>Total: <div>1,200</div></p>, open the Elements panel, and draw the tree the parser actually built.
  3. Load a page with three large images and no dimensions on a throttled connection. Measure the layout shift, then add width and height and measure again.

Useful resources

Hand picked references for this topic
Written by Lorens Mishra

Software Engineer Notes Management System Administrator

Continue reading

All HTML notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.