The Browser Hates Surprises
The Browser Hates Surprises êŽë š
We often treat the browser like a canvas â a blank slate waiting for us to paint pixels. But this mental model is flawed. The browser isnât really a painter; itâs a constraint solver.
Every time you load a page, you enter a high-speed negotiation. You provide the rules (HTML & CSS), and the browser calculates the geometry. When you give it all the math upfront, the result feels magical: a stable, buttery-smooth experience.
But when we force the browser to recalculate that geometry mid-stream â because an image loaded late, a scrollbar popped in, or a font swapped â we break the spell.
We know this phenomenon as âCumulative Layout Shiftâ (CLS), but really, itâs just jank. And jank doesnât happen by accident. It happens because we surprised the browser.
To fix this, we need to stop fighting the rendering engine and start orchestrating it. But first, seeing is believing. Letâs look at exactly what it looks like when we get it wrong.
A âHostileâ Web Site
I call what weâre doing in the demo below âhostileâ because the code is indifferent to the browserâs needs. It treats the rendering engine like a bucket we can dump data into whenever it arrives.
In the code below, you will see four distinct âsurprisesâ that break the user experience:
- The Sticky Header Collision: When you click âJump to Section 2,â the browser scrolls correctly to the top of the element, but the title gets buried behind the fixed header.
- Popcorn Loading: The text and images load independently. The layout jumps once for the text, and again for the image.
- The Image Shift: The image isnât reserved space. It pushes the text down when it finally arrives.
- The Scrollbar Shift: When the content grows long enough, a physical scrollbar pops in (on Windows/Linux), shifting the entire UI to the left.
đ The âBeforeâ Demo
Note
Note that weâre just faking this hostile behavior with setTimeout, but itâs entirely plausible that real world websites experience these conditions naturally. Data can take time to arrive from APIs. Media can be slow to load. The amount of content can push a page to needing to scroll when it didnât before.
Why did that feel so broken? It wasnât just âslow internet.â It was a failure of negotiation.
To fix this, we need to understand how the browser thinks. The browser rendering engine has a strict pipeline:
- Parse (Read HTML/CSS)
- Layout (Calculate the geometry of every box)
- Paint (Fill in the pixels)
The âStreaming Bufferâ Mistake
In the code above, we threw the text at the browser, then the image, then the scrollbar.
Every time we did that, we forced the browser to stop Painting, go back to Layout, recalculate the math for the page, and then Paint again. This is called a Reflow. Reflows are expensive for the CPU, but they are disastrous for the user experience because they physically move pixels that the user is currently looking at.
We need to move from Reactive Rendering (reacting to data arrival) to Orchestrated Rendering (planning for data arrival).
Solutions
We are going to make four specific negotiations with the browser to ensure stability.
1. The Coordinate Negotiation
The browser isnât âwrongâ when it scrolls your title behind the sticky header. It is scrolling to the exact mathematical top of the element. The problem is that our header exists outside the normal document flow.
We need to update the browserâs metadata regarding that elementâs landing zone.
:target {
/* "Hey browser, when you scroll here, leave 6rem of space against the top" */
scroll-margin-top: 6rem;
}
2. The Space Negotiation
On Windows and Linux, standard scrollbars take up physical space (usually ~17px). This happens on macOS too, but only when users have the Show scroll bars: Always option selected, which is not the default. When a scrollbar appears, the available width of the viewport changes, forcing a global recalculation that shifts centered content to the left.
We have two ways to solve this layout mutation.
Option A: The Classic Fix (Maximum Compatibility)
The most reliable method is to force the scrollbar track to be visible at all times, even on short pages.
html {
overflow-y: scroll;
}
Option B: The Modern Fix (Cleaner UI)
Modern CSS gives us a dedicated property that tells the browser: âIf a scrollbar might exist later, reserve that 17px slot now, but donât show an ugly disabled track.â
html {
scrollbar-gutter: stable;
}
3. The Layout Reservation
Normally, the browser assumes an image is 0x0 until the file header downloads. By setting an aspect-ratio in CSS, we issue a âreservation ticket.â We allow the browser to calculate the final bounding box during the CSS Parse phase â before the network request is even sent.
img {
width: 100%;
height: auto;
aspect-ratio: 16 / 9;
object-fit: cover;
}
4. The Orchestration (Promise.all)
Instead of firing three separate state updates (three separate reflows), we wait for the entire âsceneâ to be ready. We trade a few milliseconds of âFirst Paintâ for a UI that arrives fully formed.
// Don't just fetch. Orchestrate.
async function loadScene() {
// Wait for critical actors to be ready
const results = await Promise.all([
fetch('/api/text'),
fetch('/api/image')
]);
// Update the state ONCE.
// One reflow. One paint.
setFullScene(results);
}
Phase 4: The âStableâ Application
Here is the exact same application, but negotiated correctly.
Notice how âcalmâ the loading feels. The layout never jumps. The scroll lands exactly where you expect. It feels like a native application because we gave the browser the constraints it needed before painting.
đ The âAfterâ Demo
Conclusion
Optimization is not about making things load faster; it is about making them load calmer.
Every scroll bug, every jumpy image, and every layout shift is a sign that we failed to give the browser the information it needed at the time it needed it.
Stop surprising the browser. Start reserving space.