Tags: web-dev concept

Cumulative Layout Shift

Date: 2026-08-16


How much the page moved under the user without them causing it. Almost every cause is the same thing — something arrived late and nothing was holding its place — so the fix is almost always reserving space in advance.


What it is

Cumulative Layout Shift (CLS) measures unexpected movement of visible elements. Good is ≤ 0.1 at the 75th percentile — see Core Web Vitals.

It’s the only Core Web Vital that isn’t a time. Each shift scores:

layout shift score = impact fraction × distance fraction

  impact fraction    proportion of the viewport affected by moving elements
  distance fraction  the greatest distance any element moved,
                     as a proportion of the viewport

Worked. An element occupying half the viewport moves down by a quarter of the viewport height:

impact fraction    = 0.5      (the element's area before and after, unioned)
distance fraction  = 0.25
score              = 0.5 × 0.25 = 0.125     ← already over the 0.1 threshold

One shift can fail the metric. It isn’t a slow accumulation of small movements — a single banner appearing above the fold does it.

CLS reports the largest session window: shifts grouped into bursts, taking the worst burst rather than the total. That stops long browsing sessions from accumulating an unbeatable score.

The causes, in order

1. Images without dimensions. The browser can’t reserve space until the image arrives, so text reflows around it when it does.

<img src="/product.jpg" alt="…">                        <!-- shifts -->
<img src="/product.jpg" width="800" height="600" alt="…">  <!-- reserves space -->

Modern browsers compute an aspect ratio from width and height even when CSS resizes the image, so those attributes are doing real work rather than being legacy. Or set aspect-ratio in CSS.

2. Ads, embeds and iframes. Third-party content of unknown size, injected late. Reserve a container with a fixed minimum height sized to the most common case.

3. Web fonts. A fallback font renders, then the web font swaps in with different metrics and every line reflows. See Font Loading — size-adjust and metric-matched fallbacks are the fix.

4. Content injected above existing content. Cookie banners, promotional bars, “you have items in your basket” notices, personalisation blocks. Anything appearing at the top pushes everything down.

5. Late-arriving CSS. Elements render unstyled, then move when the stylesheet lands.

6. Animating layout properties. Animating height or top shifts everything after it, every frame. Use transform — see The Rendering Pipeline.

What doesn’t count

Shifts within 500ms of a user interaction are excluded, because the user caused them. This is why an accordion expanding is fine and a banner appearing on its own is not.

In plain terms: the metric is about surprise, not movement. Moving things in response to a tap is expected. Moving things while someone is reading is what makes people tap the wrong thing.

Fixing it

  • Dimensions on every image and video. The single largest win, and mechanical
  • aspect-ratio in CSS for responsive media
  • min-height on containers that will receive late content — ads, reviews, recommendations
  • Reserve the cookie banner’s space, or overlay it rather than inserting it
  • font-display: optional eliminates font-driven shift entirely, at the cost of sometimes not using your font at all. swap with a metric-matched fallback is the usual compromise
  • Insert new content below the fold, or overlay it, never above what someone is reading
  • Skeleton screens that match the final layout’s dimensions — a skeleton of the wrong size causes the shift it was meant to prevent

Diagnosing

  • DevTools → Performance → Experience track highlights each shift and names the element responsible
  • web-vitals with attribution reports the largest shift source in the field, which is what you need since CLS is often device- or viewport-specific
  • Layout Shift Regions in DevTools’ Rendering panel draws blue boxes over shifting areas as they happen — the fastest way to see it
  • Test on a slow connection. CLS is largely invisible on fast connections because everything arrives before you notice, which is why it passes in development and fails in the field

Why it matters commercially

The mistap. Someone reaches for “Add to basket”, a banner loads, and they hit “Subscribe” instead. That’s a failed interaction, a support contact, and a lost order — and it’s concentrated on mobile, where targets are close together and connections are slower. It’s the Core Web Vital with the most direct link to a specific, observable user failure.