Skip to main content

04 - CLS Prevention: Visual Stability and Layout Shift Debugging

CLS Prevention: Stabilita Visiva e Layout Shift Debugging Stai leggendo un articolo, stai per cliccare su un link - e all'improvviso tutto si sposta di...

What do you think about this article?

At a Glance

CLS Prevention: Stabilita Visiva e Layout Shift Debugging Stai leggendo un articolo, stai per cliccare su un link - e all'improvviso tutto si sposta di...

CLS Prevention: Visual Stability and Layout Shift Debugging

You're reading an article, you're about to click on a link - and suddenly everything moves 200 pixels downwards because an advertising banner has appeared above the text. You just experienced a Cumulative Layout Shift. The CLS and the Core Web metric Vitals which measures how much the contents of a page move unexpectedly during charging and use.

According to Google's CrUX data, approximately 27% of websites have a "Poor" CLS (above 0.25), which means that one in four users experiences it visually disturbing layout shifts on the most visited pages. Unlike of INP and LCP, CLS isn't about speed: it's about visual stability, an aspect of the user experience that directly impacts user frustration and trust in the site.

What You Will Learn

  • How the CLS score is calculated and what "layout shift" means
  • The Good/Needs Improvement/Poor thresholds and their SEO impact
  • The five most common causes of CLS and how to eliminate them
  • How to use Chrome DevTools and PerformanceObserver to debug shifts
  • CSS pattern with aspect-ratio, min-height and content-visibility
  • How to handle font swap, lazy loading and dynamic content without CLS

How CLS is Calculated

The CLS is not simply the number of shifts that occur: it is a composite score which takes into account both thebreadth of the shift (impact fraction) is of distance traveled by the element (distance fraction).

The formula is:

layout shift score = impact fraction * distance fraction

L'impact fraction and the percentage of the viewport involved in the shift. If an element that occupies 50% of the viewport height moves 25% of the height of the viewport, the impact fraction is 0.75 (the total area impacted before and after the shift). There distance fraction and the proportion of the viewport that the element travels during the shift.

The CLS Thresholds

Rating CLS value User Experience
Good < 0.1 Excellent visual stability
Needs Improvement 0.1 - 0.25 Some perceptible shifts
Poor > 0.25 Unstable layout, frustrated users

CLS is calculated by adding the scores of all layout shifts on the page, with one important exception: shifts caused by user interaction (scroll, click, input) are not counted. Only the shifts unexpected weigh on final score.

The Five Main Causes of CLS

1. Images and Media Without Explicit Sizes

The most frequent cause of CLS is uploading images without specifying width e height. When the browser encounters a tag <img> dimensionless, it does not reserve space in the layout. When the image loads, the browser inserts the element into the DOM and all the underlying content moves towards it the bass.

Wrong:

<img src="hero.jpg" alt="Hero image">

Correct:

<img src="hero.jpg" alt="Hero image" width="1200" height="630">

Specify width e height allows the browser to calculate the aspect ratio of the image before uploading and reserve the correct space in the layout. Modern CSS then manages responsive behavior:

img {
  max-width: 100%;
  height: auto; /* mantiene l'aspect ratio */
}

2. Web Fonts with Visually Different Fallback

When a web font is not yet downloaded, the browser displays the text with a font system (fallback). When the web font loads, the browser replaces the fallback causing a change in text size (so-called "CLS font swap"). The metric difference between a system font and a web font can be significant, causing shifts of hundreds of pixels on pages with a lot of text.

The modern solution and use size-adjust, ascent-override, descent-override e line-gap-override to align metrics of the fallback font to those of the web font:

/* Font fallback ottimizzato per Inter */
@font-face {
  font-family: 'Inter-Fallback';
  src: local('Arial');
  size-adjust: 107%;
  ascent-override: 90%;
  descent-override: 22%;
  line-gap-override: 0%;
}

body {
  font-family: 'Inter', 'Inter-Fallback', sans-serif;
}

This approach, combined with font-display: swap, reduces the CLS from font almost to zero because the fallback has the same metric dimensions as the final font.

3. Dynamic Content Injected Over Existing Content

Cookie banners, pop-up newsletters, system notifications and advertisements that appearing on top of or within the content after loading are among the causes more frustrating than CLS. The problematic pattern is to inject an element into the DOM to a position that moves the already visible content.

The rule is simple: Always reserve space before content arrives:

/* Riservare spazio per un banner ad prima che sia caricato */
.ad-container {
  min-height: 90px;  /* altezza standard banner leaderboard */
  width: 728px;
  max-width: 100%;
  /* Il banner si inserisce senza spostare nulla */
}

/* Cookie banner: posizionamento fisso non impatta il layout */
.cookie-banner {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  /* fixed non partecipa al flow del documento */
}

4. CSS Animations Using Properties That Trigger Layouts

Not all CSS animations cause layout shifts, but some do. The properties top, left, right, bottom, width, height, margin e padding They trigger browser reflow (layout) and can cause CLS if they animate elements that affect the layout of other elements.

The solution is to animate only the properties that do not trigger layout: transform e opacity. These are managed by the composer thread without involving the main thread.

/* Sbagliato: anima proprieta di layout */
.elemento-animato {
  animation: slide-sbagliato 0.3s ease;
}

@keyframes slide-sbagliato {
  from { left: -100px; }
  to { left: 0; }
}

/* Corretto: usa transform */
.elemento-animato {
  animation: slide-corretto 0.3s ease;
}

@keyframes slide-corretto {
  from { transform: translateX(-100px); }
  to { transform: translateX(0); }
}

5. Iframe Elements and Third Party Widgets Without Dimensions

Embed YouTube videos, Twitter widgets, Google maps and other third-party iframes they cause CLS if they do not have explicit dimensions. The behavior is identical to images: the browser doesn't know how much space to reserve.

<!-- Sbagliato -->
<iframe src="https://www.youtube.com/embed/..."></iframe>

<!-- Corretto: aspect-ratio moderno -->
<div class="video-container">
  <iframe
    src="https://www.youtube.com/embed/..."
    title="Titolo del video"
    allowfullscreen>
  </iframe>
</div>
.video-container {
  position: relative;
  aspect-ratio: 16 / 9;
  width: 100%;
}

.video-container iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

Debugging the CLS with Chrome DevTools

Performance Panel: Identify Layout Shifts

Chrome DevTools Performance panel is the most accurate tool for identify shift layouts. Follow these steps:

  1. Open DevTools and go to the tab Performance
  2. Click the register button (red circle)
  3. Reload the page or navigate to the section you want to analyze
  4. Stop recording after 5-10 seconds
  5. In the timeline, look for the purple bars labeled Layout Shift
  6. Click on a Layout Shift event to see the score and elements involved in the Summary section

Rendering Panel: Layout Shift Regions

An alternative way is to use the Render panel. Open DevTools, press Escape to open the drawer, select the "Rendering" tab e activate the option "Layout Shift Regions". The browser highlights in blue every area that undergoes a shift as you navigate the page.

PerformanceObserver for Production Monitoring

To collect real CLS data from users in production, use the PerformanceObserver API or the library web-vitals:

import { onCLS } from 'web-vitals';

onCLS((metric) => {
  console.log('CLS finale:', metric.value, metric.rating);

  // Le singole shift entries per il debugging
  metric.entries.forEach((entry) => {
    console.log('Shift:', {
      score: entry.value,
      elements: entry.sources?.map((s) => s.node),
      startTime: entry.startTime,
    });
  });

  // Invia a analytics
  fetch('/api/vitals', {
    method: 'POST',
    body: JSON.stringify({
      metric: 'CLS',
      value: metric.value,
      rating: metric.rating,
    }),
  });
});

Advanced CSS Patterns to Eliminate CLS

aspect-ratio: The Modern Replacement for the Padding-Bottom Hack

Before aspect-ratio, the way to preserve the aspect ratio of a container was the "padding-bottom hack". Today aspect-ratio and available in all modern browsers (Baseline 2021) and offers a solution clean and readable:

/* Vecchio approccio: padding-bottom hack */
.video-wrapper-old {
  position: relative;
  padding-bottom: 56.25%; /* 16:9 */
  height: 0;
}

/* Approccio moderno: aspect-ratio */
.video-wrapper {
  aspect-ratio: 16 / 9;
  width: 100%;
}

/* Funziona anche per card immagini */
.card-thumbnail {
  aspect-ratio: 3 / 2;
  overflow: hidden;
}

.card-thumbnail img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

content-visibility: lazy for Off-Screen Sections

The CSS property content-visibility: auto instructs the browser to skip rendering of sections that are not in the viewport. Combined with contain-intrinsic-size, allows the browser to estimate the size of the element even before rendering it, avoiding shift when the section enter the viewport:

.section-offscreen {
  content-visibility: auto;
  /* Altezza stimata della sezione */
  contain-intrinsic-size: 0 800px;
}

Skeleton Screen: Reserve Space for Asynchronous Content

For content loaded asynchronously (data from API, comments, recommendation widgets), uses a skeleton screen that replicates the structure of the final content:

/* Skeleton base */
.skeleton {
  background: linear-gradient(
    90deg,
    #f0f0f0 25%,
    #e0e0e0 50%,
    #f0f0f0 75%
  );
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
  border-radius: 4px;
}

@keyframes shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

.skeleton-title {
  height: 24px;
  width: 70%;
  margin-bottom: 8px;
}

.skeleton-text {
  height: 16px;
  width: 100%;
  margin-bottom: 6px;
}

.skeleton-avatar {
  width: 48px;
  height: 48px;
  border-radius: 50%;
}

CLS in Frameworks: Angular, React, Vue

Angular: Handle CLS during Hydration

With Angular SSR, the hydration phase (when Angular "comes to life" in the HTML server-rendered) can cause shifts if components modify the DOM in such a way which creates differences between the HTML server-side and what Angular produces client-side. The solution is to make sure that ngSkipHydration be used only for components that really can't do hydration, and that all components that show content above-the-fold have a fixed size.

Lazy Loading Images: fetchpriority="high" for LCP + loading="lazy" for the Rest

A common pattern that causes involuntary CLS to apply loading="lazy" to all images, including the LCP one. Lazy loading delays loading of the image, but the browser still has to reserve space - if they aren't there explicit dimensions, you have CLS when the image is finally loaded.

<!-- Immagine hero (above the fold): non usare lazy -->
<img
  src="hero.webp"
  alt="Hero image"
  width="1200"
  height="600"
  fetchpriority="high"
>

<!-- Immagini below the fold: lazy loading + dimensioni esplicite -->
<img
  src="product-1.webp"
  alt="Prodotto 1"
  width="400"
  height="300"
  loading="lazy"
>

CLS and Single Page Applications

In SPAs (React, Angular, Vue), client-side navigation can introduce CLS if the new "page" has elements with different sizes than the previous ones. The View Transitions API (available in Chrome and Firefox) offers a solution elegant by animating the state change instead of doing a hard replace of the DOM. See the dedicated article in the series for the complete implementation.

CLS Audit: Practical Checklist

Use this checklist to audit your site's CLS:

  • All pictures have width e height explicit?
  • Iframes and video embeds have fixed sizes or use aspect-ratio?
  • Web fonts use font-display: swap with optimized fallback metrics?
  • Do advertising banners have a container with a reserved minimum size?
  • Animations use transform e opacity instead of layout properties?
  • Does dynamic content (from API, comments) use skeleton screen or placeholder?
  • Popups and banner cookies use position: fixed o position: absolute?
  • Critical web fonts are preloaded in the <head>?

Measuring CLS at Scala: Lighthouse CI and CrUX

To monitor CLS systematically on a site with many pages, integrate Lighthouse CI in your deployment pipeline:

# lighthouserc.json
{
  "ci": {
    "assert": {
      "assertions": {
        "cumulative-layout-shift": ["error", {"minScore": 0.9}]
      }
    },
    "collect": {
      "url": [
        "http://localhost:4200",
        "http://localhost:4200/products",
        "http://localhost:4200/blog"
      ],
      "numberOfRuns": 3
    }
  }
}
# GitHub Actions: esegui Lighthouse CI su ogni PR
- name: Run Lighthouse CI
  uses: treosh/lighthouse-ci-action@v11
  with:
    configPath: ./lighthouserc.json
    temporaryPublicStorage: true

Next Steps

With INP, LCP, and CLS covered, you now have a complete understanding of the three metrics Core Web Vitals by Google. The next step is to delve deeper into the optimization of the font: the next article in the series explores font subsetting, variable fonts and the strategies font-display to maximize LCP performance e minimize CLS from font swap.

Conclusions

CLS is a metric that requires systematic attention throughout the process development, not just as post-hoc fixes. Winning patterns are simple and consistent: explicit dimensions for each visual asset, reserved space for dynamic content, animations that don't trigger layouts, fallback fonts with aligned metrics.

The good news is that solving CLS is often easier than solving INP or LCP: requires no architectural refactoring, just the disciplined application of these CSS and HTML patterns. A methodical audit with Chrome DevTools and the library web-vitals will allow you to bring your site's CLS into range "Good" in one or two work sessions.

Try These Tools

Free developer tools related to this topic.

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects