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 200 pixel verso il basso perche un banner pubblicitario e apparso sopra il testo. Hai appena vissuto un Cumulative Layout Shift. Il CLS e la metrica Core Web Vitals che misura quanto i contenuti di una pagina si spostano in modo inaspettato durante il caricamento e l'utilizzo.
Secondo i dati CrUX di Google, circa il 27% dei siti web ha un CLS "Poor" (superiore a 0.25), il che significa che un utente su quattro sperimenta spostamenti di layout visivamente disturbanti sulle pagine piu visitate. A differenza di INP e LCP, il CLS non riguarda la velocita: riguarda la stabilita visiva, un aspetto della user experience che impatta direttamente sulla frustrazione dell'utente e sulla fiducia nel sito.
Cosa Imparerai
- Come viene calcolato il punteggio CLS e cosa significa "layout shift"
- Le soglie Good/Needs Improvement/Poor e il loro impatto SEO
- Le cinque cause piu comuni di CLS e come eliminarle
- Come usare Chrome DevTools e PerformanceObserver per debuggare i shift
- Pattern CSS con aspect-ratio, min-height e content-visibility
- Come gestire font swap, lazy loading e dynamic content senza CLS
Come Viene Calcolato il CLS
Il CLS non e semplicemente il numero di shift che avvengono: e un punteggio composito che tiene conto sia dell'ampiezza dello shift (impact fraction) sia della distanza percorsa dall'elemento (distance fraction).
La formula e:
layout shift score = impact fraction * distance fraction
L'impact fraction e la percentuale dell'viewport coinvolta nello shift. Se un elemento che occupa il 50% dell'altezza del viewport si sposta di 25% dell'altezza del viewport, la impact fraction e 0.75 (l'area totale impattata prima e dopo lo shift). La distance fraction e la proporzione del viewport percorsa dall'elemento durante lo shift.
Le Soglie di CLS
| Rating | Valore CLS | Esperienza Utente |
|---|---|---|
| Good | < 0.1 | Stabilita visiva eccellente |
| Needs Improvement | 0.1 - 0.25 | Qualche spostamento percettibile |
| Poor | > 0.25 | Layout instabile, utenti frustrati |
Il CLS viene calcolato sommando i punteggi di tutti i layout shift nella pagina, con un'eccezione importante: gli shift causati dall'interazione dell'utente (scroll, click, input) non vengono conteggiati. Solo gli shift inattesi pesano sul punteggio finale.
Le Cinque Cause Principali di CLS
1. Immagini e Media Senza Dimensioni Esplicite
La causa piu frequente di CLS e caricare immagini senza specificare width
e height. Quando il browser incontra un tag <img>
senza dimensioni, non riserva spazio nel layout. Quando l'immagine si carica, il
browser inserisce l'elemento nel DOM e tutto il contenuto sottostante si sposta verso
il basso.
Sbagliato:
<img src="hero.jpg" alt="Hero image">
Corretto:
<img src="hero.jpg" alt="Hero image" width="1200" height="630">
Specificare width e height consente al browser di calcolare
l'aspect ratio dell'immagine prima del caricamento e riservare lo spazio corretto nel
layout. Il CSS moderno poi gestisce il responsive behavior:
img {
max-width: 100%;
height: auto; /* mantiene l'aspect ratio */
}
2. Font Web con Fallback Visivamente Diverso
Quando un font web non e ancora scaricato, il browser mostra il testo con un font di sistema (fallback). Quando il font web si carica, il browser rimpiazza il fallback provocando un cambiamento di dimensioni del testo (il cosiddetto "font swap CLS"). La differenza metrica tra un font di sistema e un font web puo essere significativa, causando shift di centinaia di pixel su pagine con molto testo.
La soluzione moderna e usare size-adjust, ascent-override,
descent-override e line-gap-override per allineare le metriche
del font fallback a quelle del font web:
/* 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;
}
Questo approccio, combinato con font-display: swap, riduce il CLS da
font quasi a zero perche il fallback ha le stesse dimensioni metriche del font finale.
3. Contenuto Dinamico Iniettato Sopra il Contenuto Esistente
Banner cookie, newsletter popup, notifiche di sistema e annunci pubblicitari che appaiono sopra o all'interno del contenuto dopo il caricamento sono tra le cause piu frustranti di CLS. Il pattern problematico e iniettare un elemento nel DOM in una posizione che sposta il contenuto gia visibile.
La regola e semplice: prenota sempre lo spazio prima che il contenuto arrivi:
/* 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. Animazioni CSS che Usano Proprieta che Triggherano Layout
Non tutte le animazioni CSS causano layout shift, ma alcune si. Le proprieta
top, left, right, bottom,
width, height, margin e padding
triggherano il reflow del browser (layout) e possono causare CLS se animano elementi
che influenzano il layout di altri elementi.
La soluzione e animare solo le proprieta che non triggherano layout:
transform e opacity. Queste vengono gestite dal compositor
thread senza coinvolgere il 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. Elementi Iframe e Widget di Terze Parti Senza Dimensioni
Video YouTube embed, widget Twitter, mappe Google e altri iframe di terze parti causano CLS se non hanno dimensioni esplicite. Il comportamento e identico alle immagini: il browser non sa quanto spazio riservare.
<!-- 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%;
}
Debuggare il CLS con Chrome DevTools
Performance Panel: Identificare i Layout Shift
Il pannello Performance di Chrome DevTools e lo strumento piu preciso per identificare i layout shift. Segui questi passaggi:
- Apri DevTools e vai al tab Performance
- Clicca il tasto di registrazione (cerchio rosso)
- Ricarica la pagina o naviga nella sezione che vuoi analizzare
- Ferma la registrazione dopo 5-10 secondi
- Nella timeline, cerca le barre viola etichettate Layout Shift
- Clicca su un evento Layout Shift per vedere nella sezione Summary il punteggio e gli elementi coinvolti
Rendering Panel: Layout Shift Regions
Un modo alternativo e usare il pannello Rendering. Apri DevTools, premi
Escape per aprire il drawer, seleziona il tab "Rendering" e
attiva l'opzione "Layout Shift Regions". Il browser evidenziera
in blu ogni area che subisce uno shift mentre navighi la pagina.
PerformanceObserver per il Monitoring in Produzione
Per raccogliere dati CLS reali dagli utenti in produzione, usa la
PerformanceObserver API o la libreria 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,
}),
});
});
Pattern CSS Avanzati per Eliminare il CLS
aspect-ratio: Il Sostituto Moderno del Padding-Bottom Hack
Prima di aspect-ratio, il modo per preservare il rapporto di aspetto
di un contenitore era il "padding-bottom hack". Oggi aspect-ratio
e disponibile in tutti i browser moderni (Baseline 2021) e offre una soluzione
pulita e leggibile:
/* 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 per Sezioni Off-Screen
La proprieta CSS content-visibility: auto istruisce il browser a
saltare il rendering di sezioni che non sono nel viewport. Combinata con
contain-intrinsic-size, permette al browser di stimare le dimensioni
dell'elemento anche prima di renderizzarlo, evitando shift quando la sezione
entra nel viewport:
.section-offscreen {
content-visibility: auto;
/* Altezza stimata della sezione */
contain-intrinsic-size: 0 800px;
}
Skeleton Screen: Riserva lo Spazio per Contenuto Asincrono
Per contenuti caricati asincronamente (dati da API, commenti, recommandation widgets), usa uno skeleton screen che replica la struttura del contenuto finale:
/* 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 nei Framework: Angular, React, Vue
Angular: Gestire il CLS durante l'Hydration
Con Angular SSR, la fase di hydration (quando Angular "prende vita" nell'HTML
server-rendered) puo causare shift se i componenti modificano il DOM in modo
che crea differenze tra l'HTML server-side e quello che Angular produce client-side.
La soluzione e assicurarsi che ngSkipHydration venga usato solo per
componenti che davvero non possono fare hydration, e che tutti i componenti che
mostrano contenuto above-the-fold abbiano dimensioni fisse.
Lazy Loading Immagini: fetchpriority="high" per LCP + loading="lazy" per il Resto
Un pattern comune che causa CLS involontario e applicare loading="lazy"
a tutte le immagini, inclusa quella LCP. Il lazy loading ritarda il caricamento
dell'immagine, ma il browser deve comunque riservare spazio — se non ci sono
dimensioni esplicite, si ha CLS quando l'immagine viene finalmente caricata.
<!-- 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 e Single Page Applications
Nelle SPA (React, Angular, Vue), la navigazione client-side puo introdurre CLS se il nuovo "pagina" ha elementi con dimensioni diverse da quelli precedenti. La View Transitions API (disponibile in Chrome e Firefox) offre una soluzione elegante animando il cambio di stato invece di fare un hard replace del DOM. Vedi l'articolo dedicato nella serie per l'implementazione completa.
CLS Audit: Checklist Pratica
Usa questa checklist per auditare il CLS del tuo sito:
- Tutte le immagini hanno
widtheheightespliciti? - Gli iframe e i video embed hanno dimensioni fisse o usano
aspect-ratio? - I font web usano
font-display: swapcon metriche fallback ottimizzate? - I banner pubblicitari hanno un contenitore con dimensioni minime riservate?
- Le animazioni usano
transformeopacityinvece di proprieta di layout? - Il contenuto dinamico (da API, commenti) usa skeleton screen o placeholder?
- I popup e i cookie banner usano
position: fixedoposition: absolute? - Le web font critiche vengono preloaded nella
<head>?
Misurare il CLS su Scala: Lighthouse CI e CrUX
Per monitorare il CLS in modo sistematico su un sito con molte pagine, integra Lighthouse CI nel tuo pipeline di deployment:
# 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
Con INP, LCP e CLS coperti, hai ora una comprensione completa delle tre metriche
Core Web Vitals di Google. Il passo successivo e approfondire l'ottimizzazione dei
font: il prossimo articolo della serie esplora font subsetting, variable fonts e
le strategie font-display per massimizzare le performance LCP e
minimizzare il CLS da font swap.
Conclusioni
Il CLS e una metrica che richiede attenzione sistematica durante tutto il processo di sviluppo, non solo come fix post-hoc. I pattern vincenti sono semplici e consistenti: dimensioni esplicite per ogni risorsa visiva, spazio riservato per contenuto dinamico, animazioni che non triggherano layout, font fallback con metriche allineate.
La buona notizia e che risolvere il CLS e spesso piu facile di risolvere INP o LCP:
non richiede refactoring architetturale, ma solo l'applicazione disciplinata di
questi pattern CSS e HTML. Un audit metodico con Chrome DevTools e la libreria
web-vitals ti permettera di portare il CLS del tuo sito nel range
"Good" in una o due sessioni di lavoro.







