Performance

CLS: causes and fixes

CLS measures unexpected layout movement: good under 0.1. The four causes that raise it and the fixes that show up in p75.

Key points

  • CLS adds the impact fraction and the distance fraction of unexpected shifts.
  • Shifts within 500 ms of an input do not count.
  • A low CLS does not say a shift is annoying: it says how large it is.

CLS measures how much the layout moves when nobody expects it: under 0.1 is good. The causes are images without dimensions, fonts that change metrics, banners inserted above the content and components that start at zero height.

How the score is built

Each shift is worth the impact fraction times the distance fraction: how much space the unstable element takes and how far it moves. Shifts within 500 ms of an input do not count, and the score adds up only the worst 5-second window.

The four causes that matter

CauseExampleFix
Image without dimensionsCard with a remote previewwidth, height or aspect-ratio
Font with different metricsText that switches to the webfontfont-display and a close fallback
Banner above the contentNotice inserted at runtimeSpace reserved in the markup
Component at zero heightSkeletons and lazy listsA minimum height or a sized skeleton

Attributing the shift

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.hadRecentInput) continue;
    console.log(entry.value.toFixed(4), entry.sources?.map((s) => s.node));
  }
}).observe({ type: "layout-shift", buffered: true });

In DevTools the Performance panel shows the same events as Layout Shift, with the rectangle of the element that moves. When sources is empty the shift comes from a node removed from the DOM: start from the components rendered above the fold.

Three fixes you can see in p75

  • Give every image a width and height, or an aspect-ratio: the browser reserves the space before the file arrives.
  • Reserve the height of banners and consent blocks in the markup, not with an animation on open.
  • Give skeletons and lazy lists a minimum height: a container at zero pixels opens when the data lands and pushes everything below it.

The limitation you have to name

CLS does not say whether a shift annoys the reader: a banner that moves 3 px in the same font scores almost nothing, a heading that jumps 200 px ruins the read. Our p75 of 0.012 over 209 readings describes this site, not yours, and a local registry is not the field.

Where to continue

In the catalogue: card components, image auto slider and empty state. The guides: images without dimensions and what Core Web Vitals are; the sibling article on font-display and the comparison of copy-paste and headless.

Frequently asked questions

Is CLS measured in seconds?

No. It is a unitless score from 0 upwards: it comes from the product of the fraction of space affected and the fraction of distance moved.

What is the CLS threshold?

0.1 at the 75th percentile of visits, split between mobile and desktop. Above 0.25 the page needs work.

Do deliberate movements count?

No. A movement within 500 ms of a user input is excluded, because it is an expected response rather than an unexpected jump.

Why does an image without dimensions raise CLS?

Because the browser does not know its height until the file arrives. Meanwhile the content below sits higher and jumps when the image takes its space.

Does Lighthouse measure CLS?

Yes, but on a single run without interaction. A lab CLS of zero does not rule out jumps caused by banners or data loaded later.