Errors and fixes

Layout shift from images without dimensions: the fix that costs nothing

An image with no width and height takes up zero space until it loads, so the browser pushes the rest down and CLS rises. Two attributes fix most cases.

Key points

  • Every image must declare `width` and `height`: the browser reserves the space before the download.
  • For fluid images add `height: auto` and, when the ratio changes, `aspect-ratio`.
  • CLS below 0.1 is the reference threshold, and the lab value does not always match the field one.

Short answer: declare width and height on every image. The browser reserves the space before the download, so the rest of the page does not jump when the image arrives.

The mechanism: why the space is not there

CLS sums the score of every unexpected shift: the fraction of the viewport affected multiplied by the distance moved. An image with no declared dimensions starts at zero height; when the file arrives the browser inserts it, the container grows and everything below moves. With width and height the engine works out the ratio before the network request and books a box of the same shape, so the download fills space already reserved and nothing moves. The mechanism lives in the HTML, not the CSS: the two attributes are presentational hints that the browser turns into an implicit aspect-ratio.

The diagnostic step that saves you a hunt

Open DevTools, load the page with Slow 3G throttling and read the individual shifts, not the final score. The snippet below prints the value of each shift and the node behind it: a node that is an img with no attributes points to the fix at the top of this page; a node that is a div points to a container that grows after load, and image attributes will not help. Reading the culprit before editing the markup turns a suspicion into a decision.

// incolla in DevTools ▸ Console, con la pagina già caricata
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (!entry.hadRecentInput) {
      console.log(entry.value.toFixed(4), entry.sources?.[0]?.node?.outerHTML?.slice(0, 80));
    }
  }
}).observe({ type: "layout-shift", buffered: true });

The minimal fix

The two attributes give the browser the aspect ratio, so the space is reserved before the download. height: auto lets CSS resize fluidly without distorting the image.

When the ratio changes on mobile

.cover {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
}

@media (min-width: 768px) {
  .cover { aspect-ratio: 21 / 9; }
}

aspect-ratio reserves the space even when the ratio changes at a breakpoint. Without it the container follows the image and the text below moves.

The other sources of shift

ElementWhy it shiftsFix
web fontmetrics change on load`size-adjust` on the fallback
ad or embedheight unknowncontainer with a minimum height
cookie bannerappears above the contentreserved space or overlay
lazy imageno size declaredwidth, height, aspect-ratio

How to measure it

npx lighthouse https://example.com --only-categories=performance --view
# oppure: DevTools ▸ Performance ▸ Experience

Lighthouse gives you the lab number. For field data you need the Web Vitals API, which measures real users: a low CLS on a fast lab network can be high on a slow phone.

When the fix does not work

If CLS stays high, the image may not be the culprit. Look for elements that change size after an interaction: images without dimensions shift on load, components that expand shift on click. The Performance panel shows which node caused the shift.

When the common fix makes things worse

The widespread shortcut is a fixed height on the container: height: 320px reserves the space without touching the markup. It works at one viewport and lies at every other. On a narrow screen a 16:9 image is shorter than the box and leaves an empty band; on a wide screen the box is shorter than the image, which gets cropped or overflows. CLS drops to zero and the page gets worse, so the fix is undone at the first layout complaint. The ratio, not the height, is the fact the browser needs.

A limitation with a name: art direction

With <picture> and two <source> elements serving a square crop on mobile and a wide one on desktop, the reserved ratio stays the one declared on the img: the browser picks a source later, once the space is already booked. If the mobile crop has other proportions, the shift comes back on the first phone load. The limitation is called art direction and no attribute solves it: you need an aspect-ratio per breakpoint on the container, as in the CSS block above. This is the case where the minimal fix is not enough, and it is worth saying so before the ticket is opened.

How to prevent it

  • One image component that takes width and height as required props.
  • Test on a slow network: shifts are easier to see with 3G throttling.
  • Never lazy-load images above the fold: you delay the paint and gain nothing.

Those three points stay social rules until they become an automated check. Lighthouse CI with a budget on cumulative-layout-shift turns the agreement into a merge blocker; without a budget the next image added in a hurry brings the problem back and nobody notices, because CLS raises no error.

Where to go next

In the catalogue: hero block, components and blocks. The guides: the weight of a UI library and accessibility checklist; the comparison: copy-paste or headless.

Frequently asked questions

Isn't max-width: 100% enough?

No. Fluid CSS reserves no space before the load: the browser does not know the proportions until the file arrives.

Why width and height together with height: auto?

The attributes give the ratio to the browser, height: auto lets CSS resize smoothly.

What about images with different ratios?

You need aspect-ratio on the container or the image, otherwise the reserved space does not match the final image.

Does CLS include fonts?

Yes. A font whose metrics change on load produces a shift. font-display: swap with matched fallback metrics reduces it.

How do I measure?

Lighthouse for the lab number and the Web Vitals API for field data. Read them together, never one instead of the other.