Eliminating Cumulative Layout Shift: A Practical Guide to CLS Optimization
Master CLS optimization by using CSS aspect-ratio and font-display strategies. Learn how to stop UI jitter and improve your Core Web Vitals scores today.
Last month, our team spent about three days hunting down a persistent 0.15 CLS score on a high-traffic product page. We thought the culprit was our lazy-loaded images, but it turned out to be a combination of unreserved media space and font swapping.
Improving your Core Web Vitals isn't just about chasing green scores in Lighthouse; it's about making sure your users don't accidentally click a "Delete" button because the layout jumped under their cursor. Cumulative Layout Shift (CLS) is arguably the most frustrating metric because it’s entirely invisible until the page is already rendered and the user is interacting with it.
The Role of CSS Aspect-Ratio in CLS Optimization
The most common source of layout shifts is media—images, iframes, and video embeds—that load after the initial DOM content. If you don't define the size of these elements, the browser has to reflow the entire page once the image arrives.
We used to rely on the "padding-top hack," which involved setting an element's container to a percentage-based height. It worked, but it was messy and hard to maintain. Now, we use the aspect-ratio property. It’s clean, declarative, and natively supported in every modern browser.
Here is how we fixed our hero banner grid:
CSS#9CDCFE">color:#4EC9B0">.responsive-media-container { #9CDCFE">width: 100%; #9CDCFE">aspect-ratio: 16 / 9; #9CDCFE">background-color: #f0f0f0; #9CDCFE">color:#6A9955">/* Placeholder color */ overflow: hidden; } #9CDCFE">color:#4EC9B0">img { #9CDCFE">width: 100%; #9CDCFE">height: 100%; #9CDCFE">object-fit: cover; }
By forcing the browser to reserve the 16:9 space before the image even hits the network, we effectively kill that specific source of shift. For a deeper look at managing these dynamic elements, I previously covered Cumulative Layout Shift: Fixing Dynamic Media with CSS.
Tackling Font-Display Swapping
Even if your media is perfectly sized, a slow-loading web font can trigger a massive layout shift when the browser swaps the system fallback font for your custom typeface. This is a classic "Flash of Unstyled Text" (FOUT) scenario that hits your CLS score hard.
We initially tried to hide the text until the font loaded, but that caused a "Flash of Invisible Text" (FOIT), which ruined our Largest Contentful Paint (LCP) score. The sweet spot is using font-display: swap combined with size-adjust to minimize the jump.
| Strategy | Pros | Cons |
|---|---|---|
font-display: block | No shift | High risk of invisible text |
font-display: swap | Visible text immediately | High risk of layout shift |
font-display: swap + size-adjust | Balanced experience | Requires fine-tuning metrics |
When you use size-adjust in your @font-face declaration, you can force the fallback font to match the dimensions of your custom font. It’s a bit of manual work, but it’s the only way to get near-zero shifts. I've written extensively about this in my Font Loading Strategy: Eliminate FOIT and Layout Shifts guide.
Beyond the Basics: CSS Containment
Sometimes, the shift happens because a third-party script injects content into the DOM after the page is visible. If you know an area of your page will change size, you can use the contain-intrinsic-size property to tell the browser exactly how much space to reserve.
Using contain: size or contain: layout is like telling the browser, "Don't bother recalculating the layout for this entire section; it's isolated." It’s a powerful tool for CLS optimization when dealing with complex widgets or ad slots. You can find more on this in my post about Cumulative Layout Shift Optimization: Mastering CSS Containment.
Lessons Learned
If I were to start this refactor over, I would prioritize standardizing our image aspect ratios across the design system first. We wasted a lot of time debugging specific banners only to realize that our global styles were inconsistent.
Also, don't forget to test on slow connections. Use the Chrome DevTools Network throttling (set to "Slow 3G") to see how your fonts and images behave when they don't load instantaneously. If you see your text jumping as it renders, your size-adjust values probably need another pass.
Frequently Asked Questions
1. Does aspect-ratio work for all images?
Yes, it works for any element where you want to reserve space. Just ensure you aren't fighting against fixed-height styles elsewhere in your CSS.
2. Is font-display: swap enough to stop all shifts?
Usually, no. It stops the invisible text, but the swap itself causes a jump. You almost always need size-adjust or ascent-override to align the fallback font dimensions with your web font.
3. Why is my CLS still high even after fixing images and fonts? Check for dynamic ad slots or cookie banners. These are the most common "hidden" causes of layout shifts that occur after the initial page load.
Fixing these issues is an iterative process. Start with the largest offenders, set your aspect ratios, align your fonts, and keep an eye on your real-user monitoring (RUM) data. CLS isn't a "set it and forget it" metric; it requires constant vigilance as your UI evolves.