Free lesson · Frontend System Design
The bytes arrived. Now what?
You have a stream of HTML characters landing in the browser. Your instinct - and it's a reasonable one - is that the browser "reads the HTML and draws the page." That's true the way "the engine makes the car go" is true: correct, useless, and hiding the entire thing you'd need to understand to fix it when it's slow.
Between "HTML arrives" and "pixels appear" sits a multi-stage pipeline called the critical rendering path. I'm going to claim something strong: this pipeline is the most important thing in this entire module, because almost every frontend performance decision you'll ever make is really a decision about how to not block one of these stages. Lazy loading, code splitting, why CSS goes in the <head> and scripts get defer, why you animate with transform instead of top - all of it is downstream of understanding these six stages. So let's build the mental model properly, once, and then everything else in the course clicks into place.
The six stages
graph LR
HTML[HTML bytes] --> DOM[1. DOM<br/>parse HTML into<br/>node tree]
CSS[CSS bytes] --> CSSOM[2. CSSOM<br/>parse CSS into<br/>style tree]
DOM --> RT[3. Render Tree<br/>DOM + CSSOM,<br/>visible nodes only]
CSSOM --> RT
RT --> LAYOUT[4. Layout / Reflow<br/>compute geometry:<br/>size + position]
LAYOUT --> PAINT[5. Paint<br/>fill in pixels:<br/>color, text, borders]
PAINT --> COMPOSITE[6. Composite<br/>combine layers<br/>on the GPU]
COMPOSITE --> SCREEN([Pixels on screen])
This is a left-to-right flowchart of the critical rendering path's six stages. HTML bytes are parsed into the DOM, a tree of nodes. Separately, CSS bytes are parsed into the CSSOM, a tree of styles. The DOM and CSSOM combine into the render tree, which contains only the visible nodes. The render tree goes through layout, also called reflow, which computes the size and position of every node. Then paint fills in the actual pixels: colors, text, and borders. Finally, composite combines the painted layers on the GPU, producing the pixels you see on screen.
Stage 1 - The DOM
The browser reads the HTML character by character, tokenizes it into tags, and builds the DOM (Document Object Model): a tree of nodes mirroring the structure of your markup. <body> contains a <div> contains a <p> - that nesting becomes a tree of parent and child nodes.
Two things matter here. First, DOM construction is incremental - the browser builds the tree as bytes stream in, it doesn't wait for the full document. This is why a well-structured page can start showing content before everything has downloaded. Second, the DOM is a data structure, not what you see. It has no styling and no geometry yet. It's just the skeleton.
Stage 2 - The CSSOM
In parallel, the browser parses your CSS into the CSSOM (CSS Object Model): a tree that knows the computed styles for every node, including inherited and cascaded values. If a rule says body { font-size: 16px } and another says p { font-size: 14px }, the CSSOM resolves what actually applies to each element after the cascade.
Here's the catch that drives a lot of performance advice: CSS is render-blocking. The browser will not paint anything until the CSSOM is complete, because painting an element before knowing its final styles would mean painting it wrong and repainting - a flash of unstyled content. So the browser waits. This single fact is why you put critical CSS in the <head> and why a giant blocking stylesheet delays your entire first paint. We'll dissect exactly how this blocking works in the next lesson.
Stage 3 - The render tree
Now the browser combines the DOM and the CSSOM into the render tree: every node that will actually be displayed, with its styles attached. The key word is displayed - the render tree includes only visible nodes. A node with display: none is in the DOM but not in the render tree, because it isn't painted. (Contrast that with visibility: hidden, which is in the render tree - it occupies space, it's just invisible. That distinction has real layout consequences.)
Note:
display: noneremoves a node from the render tree entirely - it takes up no space and costs nothing to lay out.visibility: hiddenkeeps it in the render tree - it still occupies its box and gets laid out, it's just not painted visibly.opacity: 0is different again - fully laid out and painted, just transparent, and (importantly) it can be composited cheaply. Three ways to "hide" something, three very different costs. Interviewers love this distinction.
Stage 4 - Layout (a.k.a. Reflow)
The render tree knows what to display and what styles apply, but not where things go or how big they are. Layout computes the geometry: the exact size and position of every node, accounting for the viewport, the box model, flexbox or grid rules, and how elements push on each other. The output is a box for every node with precise coordinates.
This stage is also called reflow, and it's expensive because geometry is relational - change the width of one element and everything around it may need recomputing. We have a whole lesson on reflow next, because controlling when it happens is one of the highest-leverage performance skills there is.
Stage 5 - Paint
With geometry known, the browser paints: it fills in the actual pixels - text glyphs, colors, backgrounds, borders, shadows. Paint often happens onto multiple layers rather than one flat image, which sets up the final stage. Painting is generally cheaper than layout, but a large or complex paint (think big box-shadows, gradients, filters over a large area) can still cost real time.
Stage 6 - Composite
Finally, the browser composites: it takes the painted layers and combines them in the correct order into the final image you see, and this work can be handed to the GPU. Compositing is the cheapest stage, and that fact is the secret behind smooth animation. If you can express a change as a composite-only operation - moving a layer, fading a layer - the browser can do it on the GPU without redoing layout or paint at all. That's why transform and opacity animate smoothly while animating width or top stutters. Again - next lesson.
Why "parallel but blocking" is the whole story
The subtle, interview-worthy insight is in how stages 1 and 2 interact. DOM and CSSOM are built in parallel, but the render tree can't form until both are ready, and nothing paints until the render tree exists. So your slowest blocking resource sets your first-paint time. A fast-parsing HTML document still can't paint if it's waiting on a slow, render-blocking stylesheet.
graph TD
subgraph Parallel["Built in parallel"]
H[HTML parsing → DOM]
C[CSS parsing → CSSOM]
end
H --> Gate{Both ready?}
C --> Gate
Gate -->|yes| R[Render tree → Layout → Paint → Composite]
Gate -->|no, still waiting<br/>on render-blocking CSS| Wait[Browser waits.<br/>Nothing paints.]
Wait --> Gate
This diagram shows why parallel work still creates a bottleneck. HTML parsing into the DOM and CSS parsing into the CSSOM happen in parallel. Both feed into a gate that asks whether both are ready. If yes, the browser proceeds to build the render tree and run layout, paint, and composite. If no, because it's still waiting on a render-blocking stylesheet, the browser simply waits and paints nothing, looping back until both trees are ready. The lesson is that the slowest blocking resource determines when the first paint can happen.
This reframes performance work as a single question you'll ask over and over: what is blocking my critical rendering path, and how do I get it out of the way? Inline the critical CSS so it's not a separate blocking request. Defer non-critical JavaScript so parsing isn't paused. Split your bundle so the browser only downloads what it needs for the first screen. Every one of those is the same move - unblock a stage.
The relationship to the metrics you'll hear about
We cover Core Web Vitals properly in the performance module, but it's worth planting the connection now, because the pipeline is what those metrics measure:
| Metric | What it really measures in pipeline terms |
|---|---|
| First Contentful Paint (FCP) | How fast you reach the first paint of any content |
| Largest Contentful Paint (LCP) | How fast the largest element finishes layout + paint |
| Cumulative Layout Shift (CLS) | Unexpected re-layouts (reflows) moving content after paint |
| Interaction to Next Paint (INP) | How fast an interaction runs through update → layout → paint |
Read that table again and notice: every metric is a stage of this pipeline, timed. You don't optimize metrics by chasing numbers; you optimize them by understanding which pipeline stage each one is watching and unblocking it.
Interview: When an interviewer asks "how would you make this page load faster?", the strongest answers are organized around the rendering path, not a random list of tips. "I'd think about it in terms of the critical rendering path: get render-blocking CSS small and inlined, defer non-critical JS so it doesn't pause parsing, and make sure the largest above-the-fold element isn't waiting on a late resource - that targets LCP directly." Structuring the answer around the pipeline shows you understand the mechanism, not just the folklore, which is what separates a senior answer from a memorized one.
A common misconception to retire
A lot of people believe JavaScript "renders the page." It doesn't - the browser's rendering pipeline renders the page. JavaScript manipulates the DOM and CSSOM, which then forces the pipeline to re-run from whatever stage you disturbed. When React updates the screen, it's not painting - it's changing the DOM, and the browser's pipeline does the layout, paint, and composite. Keeping that boundary clear is what lets you reason about why a framework can or can't make something faster: a framework can reduce how much of the DOM you touch and when, but it can't escape the pipeline. Every UI library on earth ultimately feeds this same six-stage machine.
The detail that ties it all together: one main thread
Here's the fact that turns this pipeline from trivia into the foundation of everything: almost all of this work happens on a single thread - the main thread - and that same thread also runs your JavaScript. Parsing HTML, building the DOM, recalculating styles, layout, most painting, and executing your scripts and event handlers all compete for the same one thread. The compositor often gets its own thread (which is exactly why composite-only animations stay smooth even when the main thread is busy), but the rest is a single lane of traffic.
This is the source of a category of bugs that confuses people endlessly. If your JavaScript runs an expensive function - sorting a huge array, parsing a big JSON blob, doing heavy work in a render - it holds the main thread, and while it's holding it, the browser cannot run layout, cannot paint, and cannot respond to clicks. The page freezes. Not because rendering is slow, but because rendering and your JavaScript are fighting over the same thread, and your JavaScript won.
graph LR
MT[The Main Thread<br/>one lane, shared] --> A[Parse HTML / build DOM]
MT --> B[Recalc styles]
MT --> C[Layout]
MT --> D[Paint most content]
MT --> E[Run YOUR JavaScript]
E -.->|long task blocks<br/>everything else| Freeze[UI freezes:<br/>no paint, no input]
This diagram shows the main thread as a single shared lane that must handle parsing HTML and building the DOM, recalculating styles, layout, painting most content, and running your JavaScript. Because it's one lane, a long-running JavaScript task blocks everything else, and the result is a frozen UI that cannot paint or respond to input until the task finishes.
This reframes a lot. It's why a "long task" - any script run over about 50 milliseconds - is flagged by performance tools as harmful: for that whole stretch, the page is unresponsive. It's why we break work into smaller chunks, defer non-urgent JavaScript, and move heavy computation to Web Workers (a genuinely separate thread). It's why the Interaction to Next Paint metric exists - it's literally measuring whether your JavaScript got out of the way fast enough to let a click run through layout and paint. Every one of those concerns traces back to the single fact that rendering and scripting share one thread.
Important: Keep this boundary crisp: the compositor can run on its own thread, which is why
transform/opacityanimations survive a busy main thread, but layout and paint and your JavaScript all share the main thread. So "move the animation to the GPU" and "don't block the main thread with heavy JS" are two halves of the same strategy - keep the main thread free, and push what you can to the compositor.
A worked example: optimizing a slow first paint
Let me make the whole pipeline actionable with a quick before-and-after, the kind you might narrate in a round. Suppose a page paints slowly. You open the rendering breakdown and see the first paint is waiting nearly a full second. Walking the pipeline tells you exactly where to look:
- Is render-blocking CSS the holdup? A single 200KB stylesheet in the
<head>blocks the CSSOM, and nothing paints until it's parsed. Fix: inline the critical above-the-fold CSS so first paint doesn't wait on a network round trip, and load the rest non-blocking. - Is a script pausing DOM construction? A synchronous analytics script in the
<head>freezes parsing. Fix:deferit, orasyncif it's independent. - Is the largest element waiting on a late resource? The hero image's URL is set by JavaScript, so the preload scanner never saw it and it downloads late, wrecking LCP. Fix: put the URL in the HTML (or
preloadit) so it's discoverable immediately. - Is the main thread blocked after paint? The page paints but doesn't respond to taps for 800ms because a heavy hydration script is running. Fix: split and defer that work so the main thread frees up.
Notice that every diagnosis is "which pipeline stage is blocked, and by what," and every fix is "unblock it." That's not four memorized tips - it's one mental model applied four times. When you can do this live, you sound like someone who's profiled real pages, because you have.
What you should carry out of this lesson
The critical rendering path is six stages: DOM, CSSOM, render tree, layout, paint, composite. DOM and CSSOM build in parallel but both gate the render tree, and CSS is render-blocking, so your slowest blocking resource sets your first paint. Layout (reflow) is expensive because geometry is relational; paint fills pixels; composite combines layers cheaply on the GPU, which is why the smoothest changes are the ones that touch only that last stage.
Hold onto the reframe, because it's the thread through the whole performance side of this course: frontend performance is the art of not blocking the rendering path. Now we zoom into the two parsing stages that start it all - because how the browser handles HTML versus CSS, and where your <script> tags sit, decides exactly when this whole pipeline is allowed to run.