skip to content

Parsing, DOM, and CSSOM Construction

You will learn how the HTML tokenizer builds a DOM incrementally while CSS builds a parallel CSSOM, and why CSS blocks rendering. Interviewers start critical-path questions here.

on this pageshow

questions

5

In a browser page, when does the document's DOMContentLoaded event fire, and how is that different from the window load event?

level: juniorimportance: must knowfreq 75%

answer

  1. two finish lines, not one
  2. parsing done vs everything done
  3. images are not waited for
  4. deferred scripts run just before it
  5. readyState: loading, interactive, complete

basics

~20 s

DOMContentLoaded fires as soon as the HTML has been fully parsed into a DOM and deferred scripts have run. It does not wait for images, iframes or stylesheets. The window load event fires later, once every subresource has finished loading.

solid answer

~40 s

`DOMContentLoaded` is dispatched on `document` the moment the HTML parser reaches the end of the document and the DOM tree is complete, after any `defer` scripts have executed. It deliberately ignores subresources: images, iframes, stylesheets and `async` scripts may all still be in flight. `load` is dispatched on `window` afterwards, once the document *and* all of its dependent resources have finished loading (or failed). So the ordering is always DOMContentLoaded first, load second. In practice you attach initialisation code that only needs the DOM to `DOMContentLoaded`, and reserve `load` for work that genuinely needs final geometry or images, such as measuring a laid-out image. `document.readyState` tracks the same timeline: `"loading"`, then `"interactive"` around DOMContentLoaded, then `"complete"` just before `load`.

go deeper

for a junior

Be able to state the order plainly: DOMContentLoaded when the HTML is parsed, load when images and other subresources are also done. Say which one you would use to wire up event handlers, and why.

for a middle

Explain the mechanics behind the timing: deferred scripts run before DOMContentLoaded, async scripts are not waited for, and a parser-blocking script postpones it. Map both events onto document.readyState's interactive and complete states.

for a senior

Show judgment about which signal a piece of work belongs on, and diagnose the failure mode where a late-injected script misses a one-shot event. Explain why load is a weak readiness signal on image-heavy pages.

for a principal

Own the position that neither event is a good product-level readiness metric for a modern app, and be able to argue what the team should instrument instead, and what regressions a load-based measurement will hide or exaggerate.

## Two different finish lines A page load has two milestones that people routinely conflate. `DOMContentLoaded` means *the document has been parsed*. `load` means *the document and everything it referenced has arrived*. They can be milliseconds apart on a text page and many seconds apart on a page with large images. ## What has to be true before DOMContentLoaded The HTML parser consumes the response incrementally and appends nodes to the DOM as it goes. `DOMContentLoaded` is dispatched on `document` when the parser reaches the end of the document — the DOM tree is final, nothing more will be appended by the parser. Two things can push that moment later: - **A parser-blocking classic script.** The parser stops at the script, hands control to the JavaScript engine, and only resumes afterwards. The DOM cannot be complete until that finishes. - **`defer` scripts.** Deferred scripts are guaranteed to run after parsing completes and *before* `DOMContentLoaded`, in document order. So the event waits for them. `async` scripts are not waited for. An async script may execute before or after `DOMContentLoaded` depending purely on when its download finishes — that non-determinism is the whole point of `async`. ## What DOMContentLoaded does not wait for Images, `<iframe>` documents, videos, fonts and stylesheets are all *subresources*. Their loading is independent of parsing, so `DOMContentLoaded` fires without them. This is exactly why it is the right hook for most initialisation: your query selectors and event wiring only need nodes to exist. There is one indirect coupling worth knowing. A pending stylesheet does not block the parser from building the DOM, but it *does* block a following synchronous script from executing — the script might call `getComputedStyle()`, so the browser makes it wait for the CSSOM. Since the parser is in turn waiting on that script, a slow stylesheet can delay `DOMContentLoaded` transitively. The event still has no direct dependency on CSS. ## The load event `load` fires on `window` once the document is complete and every dependent resource has either loaded or failed. A 404 image does not hang `load` forever — a failed fetch also settles the resource. Because `load` waits for the slowest image on the page, it is a poor signal for "the app is ready" and a decent signal for "final layout with real image dimensions exists". ## document.readyState The same timeline is exposed as a string on `document`: ```js document.readyState; // "loading" while the parser is running // -> "interactive" when parsing is done (DOMContentLoaded fires just after) // -> "complete" when subresources are done (load fires just after) ``` A `readystatechange` event fires on each transition. ## The late-listener trap These are one-shot events, not sticky flags. If your script runs after the event has already been dispatched — a dynamically injected script, a bundle loaded very late — the listener never fires and your initialisation silently never happens. The standard guard is: ```js function init() { /* ... */ } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } ``` ## Where to attach what - DOM wiring, event delegation, hydration of markup: `DOMContentLoaded` (or simply a `defer`red script, which runs at effectively the same point and needs no listener at all). - Measuring an element whose size depends on a loaded image, or firing analytics that should reflect a fully drawn page: `load`. - Cleanup and "the user is leaving" work: neither of these — the page-lifecycle events exist for that. ## Historical note jQuery's `$(document).ready()` and `$(window).on('load')` map onto exactly these two events, which is where a lot of the confusion originated: plenty of code used `load` when it only ever needed `ready`, and paid for it with a visibly late initialisation on image-heavy pages.

  • Does DOMContentLoaded wait for stylesheets to finish downloading?
    Not directly — it is about parsing, and CSS is a subresource. But a pending stylesheet blocks a following synchronous script from executing, and the parser is blocked on that script, so a slow stylesheet can delay DOMContentLoaded transitively. With no blocking script in the way, CSS has no effect on when it fires.
  • Your initialisation code stopped running after you moved it into a dynamically injected script. Why?
    The injected script very likely evaluated after DOMContentLoaded had already been dispatched, and these events are one-shot rather than sticky, so the listener never fires. Guard with `document.readyState === 'loading'` and call the initialiser directly otherwise.
  • Where do async scripts sit relative to DOMContentLoaded?
    Nowhere fixed. An async script executes as soon as its download completes, which may be before or after DOMContentLoaded, and the event does not wait for it. If ordering relative to the DOM matters, async is the wrong choice.

saying these in an interview costs you the question

  • Says load fires before DOMContentLoaded
  • Thinks DOMContentLoaded waits for all images
  • Believes async scripts delay DOMContentLoaded
  • Attaches the listener late and expects it to still fire
  • Treats window.onload as the general app-ready hook

context

open as a page

Why is a `<link rel="stylesheet">` in a page's `<head>` described as render-blocking, and what is the browser doing while that file downloads?

level: middleimportance: must knowfreq 70%

basics

~20 s

The browser will not paint until it has a complete CSSOM, because painting with partial styles would show unstyled content and then repaint it. Meanwhile the HTML parser keeps running and building the DOM — only rendering is held, not parsing.

open as a page

What is the browser's preload scanner (also called the speculative or lookahead parser), and which resources on a page can it not discover?

level: middleimportance: should knowfreq 45%

basics

~20 s

The preload scanner is a secondary, lightweight parser that scans the raw HTML ahead of the main parser and starts downloading resources it finds in markup attributes. It cannot see anything that only exists after CSS or JavaScript runs.

open as a page

Walk through how a browser turns an incoming stream of HTML bytes into a DOM tree, and explain why it does not wait for the whole document to arrive first.

level: middleimportance: should knowfreq 52%

basics

~20 s

Bytes are decoded to characters, a tokenizer state machine turns those into start-tag, end-tag, text and comment tokens, and a tree constructor turns tokens into nodes and attaches them to the DOM. It runs on each chunk as it arrives so the page can render before the document ends.

open as a page

A page loads one stylesheet from its head, and that file begins with `@import url("theme.css")`. Why does first paint arrive later than if both files had been linked from the HTML head?

level: seniorimportance: should knowfreq 36%

basics

~20 s

An @import is only discovered after the parent stylesheet has been downloaded and parsed, so the two requests run one after the other instead of in parallel. The preload scanner cannot see inside CSS, so nothing starts the second fetch early.

open as a page