How browsers render HTML, CSS, and JS
The browser parses HTML into the DOM and CSS into the CSSOM, combines them into the render tree, runs Layout (geometry), Paint (pixels), and Composite (assemble layers). JS can mutate the DOM/CSSOM, so a synchronous <script> blocks parsing; CSS blocks rendering and (without async) blocks the script after it. This is the Critical Rendering Path.
When the browser receives a page, it runs a multi-stage pipeline — the Critical Rendering Path — to turn bytes into pixels.
The stages
1. Parse HTML → DOM The HTML parser builds the DOM tree incrementally as bytes arrive. When it hits a <script> (synchronous), it pauses parsing, downloads, and executes it — because JS can change the DOM.
2. Parse CSS → CSSOM CSS is parsed into the CSSOM tree. CSS is render-blocking: the browser won't paint until the CSSOM is ready (otherwise you'd get a flash of unstyled content). CSS is also parser-blocking for scripts — a synchronous <script> after a <link rel=stylesheet> waits for that CSS, because the script might read computed styles.
3. Render tree DOM + CSSOM are combined into the render tree — only the nodes that will be visible. display: none nodes are excluded; visibility: hidden ones are included (they take space).
4. Layout (reflow) The browser computes the geometry — exact position and size of every render-tree node. Viewport-dependent.
5. Paint Fill in pixels: text, colors, borders, shadows, images — onto layers.
6. Composite Assemble the painted layers in the right order (respecting z-index, transforms, opacity) into the final on-screen image. The GPU helps here.
HTML ─▶ DOM ─┐
├─▶ Render Tree ─▶ Layout ─▶ Paint ─▶ Composite ─▶ screen
CSS ──▶ CSSOM┘Where JS fits — and blocks
- A plain
<script>blocks HTML parsing while it downloads and runs. <script defer>— downloads in parallel, runs after parsing, in order. Best default for app scripts.<script async>— downloads in parallel, runs as soon as it's ready (can interrupt parsing), order not guaranteed. Good for independent third-party scripts.<script type="module">— deferred by default.- JS that reads layout (
offsetHeight) or mutates the DOM can trigger reflow/repaint — and can cause layout thrashing if it interleaves reads and writes.
Why this matters
- Put CSS in the
<head>(render-blocking — you want it early); put scripts at the end of<body>or usedefer. - Minimize render-blocking CSS/JS to speed up First Contentful Paint.
- Inline critical CSS, defer the rest.
Senior framing
The senior answer ties the pipeline to performance levers: CSS is render-blocking and can block scripts; sync scripts block parsing; defer/async are the fix. And the back half — Layout → Paint → Composite — is the same pipeline that governs animation cost. Knowing that one model explains both initial load and runtime jank is the depth signal.
Follow-up questions
- •What's the difference between async and defer?
- •Why is CSS render-blocking, and is that good or bad?
- •Why does a synchronous script after a stylesheet wait for the CSS?
Common mistakes
- •Thinking JS never blocks HTML parsing.
- •Putting render-blocking scripts in the <head> without defer/async.
- •Confusing the render tree with the DOM (display:none differs).
- •Not knowing CSS can block scripts, not just rendering.
Edge cases
- •Inline scripts can't be deferred; they block immediately.
- •Preload scanner fetches resources ahead even while parsing is blocked.
- •type=module scripts are deferred and run after parsing.
Real-world examples
- •Optimizing FCP/LCP, deciding script loading strategy, debugging FOUC.