Of all the web performance metrics, Cumulative Layout Shift is the one that most closely matches how bad a site feels. Everyone has tried to tap a link, had an image load above it, and hit an ad instead. That's CLS.
Google considers anything above 0.1 poor. The Clash of the Titans site, on Google Sites, measured 0.51 — roughly half the viewport's worth of movement while loading. After the rebuild it measured 0. Here's where that number came from.
Almost all of it is images without dimensions
The dominant cause of layout shift on content sites is boringly consistent: an image whose height the browser doesn't know until the bytes arrive.
The browser lays out the page, leaves a zero-height box where the image goes, paints the text, and then — when the image finally arrives — discovers it needs 400 pixels and shoves everything below it down the page. Every image without declared dimensions is a small earthquake scheduled for whenever the network delivers.
The fix is old and unglamorous: tell the browser the aspect ratio up front so it can reserve the right space before it has the file.
<!-- shifts: browser learns the height only when the bytes land -->
<img src="/hero.jpg" alt="">
<!-- doesn't shift: space is reserved during the first layout pass -->
<img src="/hero.jpg" alt="" width="1600" height="900">The second cause is fonts, and it's sneakier
A web font that loads late gets swapped in for a fallback that has different metrics. Every line of text subtly re-flows, and if the fallback wraps to a different number of lines than the real font, every element below the text block moves too.
This one is nastier to diagnose because it's invisible on a fast connection and on a warm cache — which is to say, invisible on the developer's machine. It shows up on a cold mobile visit, which is most real first visits.
Preloading the font and pairing it with a metric-compatible fallback shrinks the swap to something imperceptible. Next.js handles most of this if you use its font loader rather than a stylesheet link, which is one of the genuinely load-bearing reasons to use the framework's own primitives instead of routing around them.
The third cause is anything injected above the fold
Banners, cookie notices, promo bars, embedded widgets, A/B test variants. Anything that appears at the top of the page after first paint pushes the entire document down.
Page builders are especially prone to this, because they ship a generic runtime that decides late what the page contains. That's a structural disadvantage rather than a bug — and it's a large part of why platform sites tend to plateau on this metric no matter how much you tune inside them.
- Reserve the space if the element is genuinely conditional — an empty slot that sometimes fills is far better than an insertion that always shoves.
- Render it server-side if you can, so it's present in the first paint rather than arriving after it.
- Overlay rather than insert, when the element is dismissible anyway.
Why zero, and not just "good"
0.1 is the threshold for a passing grade, and there's a reasonable argument that chasing 0 past that point is diminishing returns. In practice, on a fresh build, zero is easier to hold than a small non-zero number — because every shift has an identifiable cause, and "none" is a much simpler invariant to defend than "under 0.1 on the devices we checked."
Both sites I've rebuilt came in at 0 (from 0.51 and 0.01 respectively). Not because of heroic optimisation, but because declaring image dimensions and loading fonts properly from the start is nearly free — whereas retrofitting it onto an existing site means auditing every image on every template.
Layout shift is the cheapest performance win available on most sites, and it's the one visitors actually notice. If you only fix one metric, fix this one — and if you want to know what your current site scores, the free audit below will tell you in about thirty seconds.
This post draws on a real build. Read the Clash of the Titans case study →
Related. grade your own site, free → · the $750 Site Tune-Up →
Next up. The admin panel I didn't write →