DEV Community

137Foundry
137Foundry

Posted on

Why Cumulative Layout Shift Is Harder to Fix Than It Looks

Of the three Core Web Vitals, Cumulative Layout Shift often surprises developers. LCP is a loading problem. INP is a JavaScript problem. CLS looks like it should be solved by adding width and height to images, and then it turns out your score barely moves after you do that.

The reason CLS is deceptively hard is that the score accumulates from multiple sources, most of which are not images. A CLS score is the sum of all unexpected layout shifts during a page session, and the sources include web fonts, injected third-party content, dynamic banners, browser-extension interference, and animated elements that change dimensions without user interaction. Fixing images helps. It usually isn't enough on its own.

How CLS Is Actually Calculated

The metric combines the "impact fraction" (how much of the viewport moved) with the "distance fraction" (how far it moved). A shift that moves a large block of content a short distance can score similarly to one that moves a small element a long distance. Understanding the formula matters because it reveals that small-looking shifts can have significant score impact if they affect large portions of the viewport.

CLS is also session-windowed. The browser groups shifts into windows of up to 5 seconds, with a maximum gap of 1 second between shifts. The score for a window is the sum of all shifts in that window. Your reported CLS is the worst window's score.

This windowed approach means that a series of small shifts that happen in quick succession can accumulate into a high CLS score, even if no single shift is dramatic. Pages that load ads or dynamic content in stages often have this pattern. What looks visually like minor movement repeatedly is registering as a high CLS window score.

construction scaffolding building frame assembly
Photo by 2211438 on Pixabay

The Sources Most Teams Miss

Web font loading. When font-display: swap is set, the browser renders text in the fallback font first, then swaps to the web font when it loads. If the web font has different metrics than the fallback (different x-height, character width, or line height), the swap causes a layout shift. This is a common source of CLS on editorial sites and landing pages that use distinctive typography.

The fix is either to use font-display: optional (no swap at all, web font only used if loaded in the first render frame) or to use the CSS size-adjust, ascent-override, descent-override, and line-gap-override descriptors to align the fallback font metrics to the web font. The latter approach is more complex but preserves the web font experience for users on fast connections.

Ads and embeds. Ad networks and embedded third-party widgets frequently insert content into reserved or unreserved slots after the page has painted. A cookie consent banner that appears above the page header is a classic example. An ad that loads into a container without a declared minimum height is another.

For ad slots, always declare a minimum container height in CSS matching the expected ad dimensions. For consent banners and notifications, wrap them in a container with a declared minimum height even if you plan to animate them in. The declared height prevents the shift on insertion.

Animations. CSS animations that change top, left, margin, or padding cause layout shifts because those properties affect how the browser positions surrounding elements. Animations that use transform: translate() do not cause layout shifts because transforms operate in the compositor layer and don't affect layout flow. Review any CSS that animates position-affecting properties and convert them to transform-based equivalents.

Dynamically loaded content above existing content. Any JavaScript that inserts content above already-painted content will cause a layout shift if the existing content moves down. This includes chat widgets that appear in a fixed position but whose initialization changes layout, notification banners inserted at page top, and lazy-loaded content sections above the fold that load after user scroll.

Why Tools Don't Always Catch Everything

Lab tools like Lighthouse capture CLS from a single simulated page load in a clean browser context. They can't reproduce a shift caused by a browser extension that adds toolbar content. They can't always reproduce shifts from ad networks whose ad content varies by visitor or by geographic location. And they don't simulate the range of user devices and system fonts that affect font rendering and fallback behavior.

Field data from Google Search Console and the Chrome User Experience Report captures the actual 75th-percentile CLS across real visits. If your lab CLS score is good but your field data shows poor CLS, the problem is almost certainly coming from a source that varies by visitor, like ads, extensions, or system font differences between operating systems.

MDN Web Docs has detailed documentation on the Layout Instability API, which is what the browser uses to measure CLS. Reading the spec-level description of what constitutes a layout shift and what doesn't is useful when you're debugging an unexpected score. Not every visual change counts as a shift; only unexpected changes to the start position of layout-affecting elements in the viewport trigger the metric.

Where to Start Debugging

The Chrome DevTools Performance panel shows layout shifts as events in the timeline. Click on any shift event to see which elements moved and what triggered the shift. The "Experience" row in the timeline highlights shift events specifically, and clicking into one shows the affected elements and the computed CLS score contribution from that event.

For shift events that happen after initial load (caused by ads, dynamic content, or JavaScript-triggered changes), set up a longer recording in DevTools that captures user interaction or page scroll. Some shifts only appear when the user interacts with specific page elements or when a page reaches a certain scroll position. Record a realistic user session rather than just the initial page load.

layout grid wireframe digital screen design
Photo by Google DeepMind on Pexels

Start with the shifts that appear in the first 2.5 seconds of page load, since those tend to have the highest impact on the reported score. Work through the sources in order: images without explicit dimensions, web font swaps, injected content, and animated elements using layout-affecting properties. Use a real device on a slower network profile to surface shifts that only appear under load conditions.

Verifying the Fix

After applying CLS fixes, measure the change in both lab tools and, over time, in field data. For font-related fixes, disable web fonts entirely in DevTools and check whether the content changes visually. If the text barely changes appearance, font-display: optional is safe to deploy. For ad-slot fixes, use DevTools' network throttle to load the page slowly and observe whether content shifts when ads inject.

Keep in mind that a CLS fix on one template doesn't mean the same fix applies everywhere. A site with multiple page types may have different CLS sources on article pages versus category pages versus landing pages. Audit each template type separately.

For a broader look at all three Core Web Vitals metrics and how to address LCP and INP alongside CLS, 137Foundry put together a complete production audit guide: How to Audit and Fix Core Web Vitals on a Production Website.

"CLS is the metric where I see teams get stuck longest. They fix the images, run Lighthouse again, see a slightly better number, and assume it's done. But field data still shows poor. The remaining score almost always comes from font swaps, third-party injections, or animations that nobody thought of as layout-affecting." - Dennis Traina, founder of 137Foundry

CLS requires systematic enumeration of every source of visual change on a page, not just the obvious ones. The good news is that once you've worked through that enumeration on a given page template, the fixes usually stay fixed. Unlike INP, which can regress with any new JavaScript, CLS problems are structural and tend to stay solved once the root cause is addressed properly.

Top comments (0)