If your Page Speed Grader report shows cls: 0.18 or higher, you've got a fixable problem. Cumulative Layout Shift is almost always caused by four specific things, and each one has a deterministic fix that doesn't require a full rebuild or a JavaScript refactor.
This post is the order in which to fix them. Start at the top; most sites are clean by the time they get to step 3.
CLS is the sum of every layout shift that happens during page load, weighted by the size of the shift and the proportion of the viewport affected. The three thresholds:
Pass: CLS ≤ 0.1
Needs improvement: 0.1 to 0.25
Fail: CLS > 0.25
A page at 0.25 has visible content jumping multiple times during load. A page at 0.1 has a noticeable but minor shift. A page at 0.05 feels stable.
CLS only counts shifts that happen without user input. Clicking a "Show more" button that expands content is not a layout shift in the CLS sense — that's user-initiated. The shifts that count are the involuntary ones: text reflowing when a font swaps, images pushing content down when they finally load, ads injecting themselves below the fold.
The single largest source of CLS on real sites: <img> tags without width and height attributes. The browser doesn't know how much space to reserve, so it lays out the page assuming zero space, then jolts everything down when the image arrives.
<!-- Bad: browser reserves no space until the image loads --><img src="/hero.jpg" alt="..."><!-- Good: browser reserves the right aspect ratio immediately --><img src="/hero.jpg" alt="..." width="1200" height="630">
CSS-only sizing is not enough. The browser uses the width and height attributes specifically to compute the aspect ratio before image data arrives — they have to be on the element.
For responsive images, this is what aspect-ratio was built for:
img { width: 100%; height: auto; aspect-ratio: attr(width) / attr(height); /* derived from the attributes */}
This pattern works in every modern browser and handles every responsive case.
To find offending images: the Page Speed Grader's CLS section reports the elements responsible for shifts. The grader names each one. If you see <img class="hero"> in the report, that's the offender.
Web fonts loaded with font-display: swap show a fallback font until the web font arrives, then swap. If the fallback metrics differ from the web font (which they almost always do), every line of text reflows when the swap happens.
For body text that's the entire page, the shift can be 0.05–0.10 on its own.
Three approaches, in order of effectiveness:
Best: font-display: optional. The browser uses the web font only if it loads in the first 100ms. After that, it sticks with the fallback. No swap, no shift.
Tradeoff: on slow connections, users see only the fallback. For most sites this is acceptable; the visual difference between Inter and the system sans-serif fallback is minor compared to the layout-shift cost of swapping.
Second-best: matched fallback metrics. Use the size-adjust, ascent-override, descent-override, and line-gap-override descriptors in @font-face to make the fallback render at the same dimensions as the web font. When the swap happens, line lengths and heights stay constant.
This requires measuring both fonts and computing the override values. The Mozilla DevTools fonts panel has a generator; for production sites, the fontaine or next/font Node packages handle this automatically.
Acceptable: preload critical fonts.<link rel="preload" as="font"> for the one or two fonts above the fold. The font arrives before the browser tries to render text, so the swap happens earlier and shifts less.
Late-injected UI elements that push content around are a classic CLS source. Common offenders:
Cookie consent banners that appear after the page loads
Chat widgets (Intercom, Drift, Crisp) that pop in from the bottom right
Newsletter popups
"Sale ends in" sticky bars
If any of these appear after the initial layout, they cause a shift.
The fixes:
Cookie banners: reserve the space. If the banner is going to take 80px at the top, render an 80px-tall placeholder before the banner JavaScript runs. The placeholder can be styled to look like the loading banner. Once the real banner mounts, it fills the same space — no shift.
Chat widgets: position absolutely or fixed. Chat widgets should be position: fixed and not part of the document flow. If your chat widget is shifting content, it's positioned wrong — fix the CSS, not the timing.
Sticky bars and popups: render server-side or trigger only on user action. A bar that's part of the initial HTML doesn't shift. A popup that fires on scroll past the fold doesn't count toward CLS (because it's user-initiated).
Ads injected by ad networks and embedded content (YouTube, Twitter, Instagram embeds) tend to inject themselves into the page after layout. The result is the most visible kind of CLS — content jumps as ads load.
For ads:
Reserve the slot. Ad networks specify the dimensions of each ad unit. Render a div with those exact dimensions before the ad loads.
Constrain dynamic ad sizes. Some ad networks serve variable-height ads. Either constrain them to a fixed dimension or move the ad slot to a position where shift doesn't matter (e.g., after the article content rather than within it).
For embeds:
Use the embed provider's official aspect-ratio container. YouTube's <iframe> should be wrapped in a 16:9 container.
Lazy-load below-the-fold embeds. A Twitter embed at the bottom of a 3000-word article doesn't need to load on initial render.
After making changes, re-run the Page Speed Grader. Look at the CLS field specifically — the score should drop to under 0.1.
For real-user verification, check Search Console's Core Web Vitals report. Field data takes 28 days to update, so don't expect immediate feedback. The lab metric from the grader is your in-the-moment signal.
If you've fixed images, fonts, banners, and ads/embeds, and CLS is still above 0.1, the remaining cause is almost always one of:
A third-party script injecting DOM elements late (heatmap tools, A/B testing platforms, analytics with custom widgets)
Server-side rendering mismatches with client-side hydration (the React/Vue/Angular tree doesn't match what the server sent, so the client re-renders and shifts content)
window.location.hash jumps that scroll the page during initial render
These are harder. For most sites, the first four fixes are enough.