Lesson 1 of 5
What Happens When You Load a Page
"Civilization advances by extending the number of important operations which we can perform without thinking about them." — Alfred North Whitehead, An Introduction to Mathematics (1911)
A browser does an extraordinary amount of work between you pressing Enter and a page appearing, and its greatest achievement is that you never have to think about any of it. Whitehead's line is a compliment to that: an abstraction good enough to be invisible is an abstraction that lets you build things instead of managing details.
It is also, for a frontend engineer, exactly the problem. The moment a page is slow, or an element is in the wrong place, or a style refuses to apply, or a screen reader announces nothing — you are looking at a symptom produced somewhere inside the machinery you have been trained not to think about. And the difference between an engineer who fixes it in ten minutes and one who spends an afternoon changing things at random is almost entirely whether they can name the stage where it went wrong.
So this volume starts by making the invisible legible. Not exhaustively — you will never need to implement a layout engine — but well enough that every later chapter has somewhere to attach. When Part III explains why a Flexbox child overflows, or Part VI explains why an image shifts the page, or Part IV explains why display: none hides content from a screen reader too, each of those is a fact about a specific stage of this pipeline.
The chapter's practical payoff is narrower than the theory and worth stating up front. Two facts about this pipeline cause most of what you will experience as slowness: stylesheets block rendering, and synchronous scripts block parsing. Everything else in Part VI is a refinement. If you take nothing else from this chapter, take the timeline that shows what those two cost.
Your HTML, CSS and JavaScript all arrive in a few hundred milliseconds, and the page still takes noticeably longer than that to appear. Nothing is slow on its own. Where did the time go — and why is the answer almost always about what was waiting rather than what was working?
The pipeline, in order
Seven stages. The network half was Volume 0's subject; from the first byte onward is ours.
Run to execute the tests…
The third test is the one that surprises people. A synchronous script waits for outstanding stylesheets before it runs — because it might read a computed style and would otherwise read the wrong one. So a slow stylesheet delays a script, which delays the parser, which delays everything. Three stages coupled by one download.
Think about a kitchen plating a dish. The chopping can start the moment ingredients arrive, and it carries on while the sauce reduces — those are independent. But nothing goes on the plate until the sauce is ready, because plating needs it. Now add one rule: a particular prep step has to wait for the sauce, because it needs to taste it first. Suddenly a slow sauce stops not just the plating but the chopping too, and a kitchen that looks busy is producing nothing. That is a synchronous script waiting on a stylesheet. The work was never the bottleneck; the ordering was.
The render tree is not the DOM
This distinction explains a whole family of confusions, and it is easiest seen than described.
Change display: none to visibility: hidden on box 2 and watch the gap appear. That is the render tree, visible: display: none removes a node from it entirely — no box, no space, no geometry — while visibility: hidden keeps the box and simply does not paint its contents.
This is not trivia. It is why a display: none element is invisible to a screen reader (Part IV), why toggling it forces a full layout while toggling opacity does not (Part VI), and why measuring a hidden element returns zero.
What the two blocking rules actually cost
Now the timeline, with the numbers computed rather than asserted.
Run to execute the tests…
The fourth test is the finding worth carrying into Part VI. Once the script stops blocking, first paint is gated entirely by the stylesheet — parsing finished 105ms earlier and the page still waits. Deferring the script did not make CSS the critical path; it revealed that CSS always was.
And the fifth is the corrective for the instinct everyone has: halving the HTML saves 13ms against the 105ms sitting in the ordering rules.
Layout, paint, composite — and why the distinction pays
The last three stages are the ones you can sometimes skip, and skipping them is the difference between a smooth animation and a janky one.
Run to execute the tests…
The fourth test states it plainly: the same visual motion costs three stages or one, depending only on which property you animate. top and transform move an element identically on screen; one recomputes layout every frame and the other repositions an already-painted layer.
Seeing it yourself
The pipeline is not theoretical — the browser will tell you about every stage.
Note the gap between DOM interactive and first contentful paint — that is the render tree, layout and paint happening after the DOM was ready. Every metric in Part VI is built from these primitives.
The blocking behaviour of scripts is not a design flaw so much as a fossil. Early browsers had to support document.write, which lets a script inject markup at its own position in the stream — so the parser genuinely cannot know what follows a <script> tag until that script has run. That single feature, essentially unused in modern code, is why synchronous scripts still stop the parser today. The defer and async attributes arrived to let authors opt out, and browsers later added speculative pre-parsing that scans ahead for resources to download while the parser is stalled — a mitigation, not a fix, since the DOM still cannot be built past the stalled point. It is a good example of how the web evolves: the compatible-with-everything path stays the default and the better behaviour is opt-in, which is why so much of frontend performance work is really the work of opting in.
The habit that has saved me the most time is asking, before touching anything, which stage a symptom belongs to. "The layout jumps after a second" is layout, so I look for something arriving late without reserved space. "The style doesn't apply" is the CSSOM, so I look at the cascade before I look at the selector. "It's smooth on my machine and janky on a phone" is almost always layout-during-animation, so I check which property is being animated before I optimise anything else. That question takes five seconds and it eliminates most of the search space. The failure mode I have watched repeatedly — and been guilty of — is starting to change code before naming the stage, which turns a diagnosis into a series of experiments, each of which invalidates the last.
A Lagos team spent two weeks on a page that "felt slow", compressing images and minifying CSS, and moved their first paint by under 40ms. When someone finally opened the performance panel rather than optimising by instinct, the trace showed the parser idle for roughly 400ms partway through the head: a third-party analytics tag, loaded synchronously, sitting behind a stylesheet from another domain. Adding async to one script tag moved first paint by nearly half a second — more than everything else they had done combined, in a one-line change. The lead's note afterwards was the useful part: they had optimised the things that were visible in the codebase — big files, obvious assets — and the actual cost was a gap, which is invisible unless you look at a timeline. Nothing in the source told them the parser was waiting.
Open a site you use daily, record a load in the performance panel, and find the parser. Not the total time — the gaps. Where is the main thread idle, and what is it waiting for? For most sites you will find at least one stall of a few hundred milliseconds with nothing running, and the resource responsible will usually be something nobody chose deliberately: a font, a tag manager, a widget added for a campaign two years ago. Then the sharper question: how would anyone on that team have known? Nothing in their source code says "the parser waits here". It is only visible in the timeline, which is precisely why performance work that skips measurement optimises the wrong things so reliably.
Common Mistakes
Optimising before naming the stage. "Slow", "jumpy", "the style won't apply" and "janky animation" belong to four different stages with four different fixes.
Confusing render-blocking with parse-blocking. CSS delays the pixels; a synchronous script delays the DOM itself.
Forgetting that a sync script waits for pending CSS. One slow stylesheet delays a script which delays the parser — three stages coupled by one download.
Assuming the render tree is the DOM.
display: noneremoves a node from the render tree entirely: no box, no space, no measurable geometry, and nothing for a screen reader.Reaching for file size first. In the worked timeline, halving the HTML saved 13ms against 105ms sitting in the ordering rules.
Animating
top,left,widthorheight. Identical motion totransform, at three stages instead of one, on every frame.Believing "animate transform and opacity" is a style rule. It is a statement about which stages those properties trigger — and those two are the only composite-only properties there are.
Ignoring the frame budget. 60fps leaves under 13ms of your own work per frame; a layout pass over a large tree can exceed that alone.
Measuring an element that is
display: none. It has no geometry, so you get zeros.Trusting a fast local machine. Layout cost scales with tree size and device power, and the stage that suffers first is the one you cannot see in the source.
Interview Corner
Open the performance panel on a page you own and record a load. Find the parser gaps, not the big files — then work out what each gap is waiting for. Next, use the timeline block above on your own numbers: put your real TTFB, stylesheet and script timings in and see what deferring the script would buy. Then prove the last section to yourself: build a small element and animate it twice, once with top and once with transform, and watch both in the performance panel with CPU throttled to 4×. The first will show layout on every frame and the second will not, and once you have seen that trace the rule stops being something you remember and becomes something you understand.
Run to execute the tests…
Summary
- Seven stages: parse HTML → DOM, parse CSS → CSSOM, run JS, build the render tree, layout, paint, composite. Every visual bug and slow page attaches to one of them.
- Naming the stage is most of the debugging. Four different symptoms, four different stages, four different fixes.
- CSS blocks rendering, not parsing. The DOM keeps building while a stylesheet downloads; the pixels wait.
- A synchronous script blocks parsing — a
document.writefossil — and waits for pending stylesheets, coupling three stages to one download. - The render tree is not the DOM.
display: noneremoves a node entirely: no box, no space, no geometry, nothing announced. - In the worked timeline, deferring one script moved first paint 105ms (a third faster), while halving the HTML would have saved 13.
- Once scripts stop blocking, CSS is the critical path — deferring reveals that rather than causing it.
- Layout → paint → composite are skippable from the bottom. Geometry triggers all three, colour two, and
transformandopacityare the only composite-only properties. - The same motion costs three stages or one, depending only on the property animated.
- 60fps leaves under 13ms per frame for your own work, which a single large layout pass can exceed.
In the next chapter, The DOM & the Render Tree, we go a level deeper into the two structures this one introduced — how the DOM is actually built and mutated, what the render tree contains that the DOM does not, and why the cost of touching either one is where most JavaScript performance problems begin.
What strikes me writing this is how much of frontend performance is not about speed at all. Every number in this chapter came from ordering rather than throughput — the parser was not slow, it was idle; the animation was not inefficient, it was triggering a stage it did not need. That is a strange thing about the browser as a platform: the machine is extraordinarily fast and most of what we experience as slowness is coordination overhead we introduced without meaning to. Whitehead's observation about not having to think was about the freedom that abstraction buys, and he was right — nobody should have to think about the render tree to put a heading on a page. But the corollary he did not need is that when the abstraction leaks, the only people who can fix it are the ones who kept a model of what was underneath. That model is cheap to acquire and it is what this whole volume is for.
Q&A · 0
Enrol to ask questions and join the discussion.
No questions yet — be the first to ask.