Prevenirea CLS: Stabilitate vizuală și Depanare prin schimbarea aspectului
Citiți un articol. Sunteți pe cale să faceți clic pe un link interesant. Deodată conținutul face clic în jos cu 200 de pixeli: apare un banner publicitar mai sus. Ai dat clic pe linkul greșit. Acest fenomen — schimbare de layout — și exact ce Schimbare cumulativă a aspectului (CLS) măsură. Google îl consideră unul dintre cele mai importante trei elemente vitale Core Web pentru experiența utilizatorului.
Un CLS sub 0,1 este considerat „bun”; între 0,1 și 0,25 „a îmbunătăți”; peste 0,25 „rar”. Vestea bună: spre deosebire de LCP (care necesită optimizări de rețea și server), CLS depinde aproape în întregime de CSS și HTML. Cu tehnicile potrivite poți aduce CLS-ul la zero.
Ce vei învăța
- Cum calculează browserul CLS: fracția de impact, fracția de distanță, scor
- Identificați cauzele CLS cu regiunile de schimbare a aspectului în Chrome DevTools
- Imagini fără dimensiune: vinovatul numărul unu
- aspect-ratio CSS: rezervă spațiu înainte ca imaginea să se încarce
- Schimbarea fonturilor CLS: afișarea fontului și ajustarea dimensiunii pentru valorile de rezervă
- Conținut injectat dinamic: reclame banner, consimțământ pentru cookie-uri, ecrane schelet
- Animații care provoacă CLS: transformare vs sus/stânga
Cum se calculează CLS
CLS nu măsoară câte ture apar, ci cât de mult „deranjează” utilizatorul. Fiecare schimbare de aspect produce un scor calculat astfel:
scor shift layout = fracțiune de impact × fracțiune de distanță
L'fracția de impact și procentul din fereastra care este „impactată” din tură (zonă ocupată înainte + zonă ocupată după). Acolo fracțiune de distanță și distanța maximă parcursă de orice element afectat, sub formă de fracție a dimensiunii ferestrei de vizualizare. CLS final este suma tuturor schimburilor a avut loc în ferestre de sesiune de 5 secunde, luând maximum.
// Misura il CLS con PerformanceObserver
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// Ignora shift causati dall'interazione utente (scroll, click)
// Gli shift da input utente non vengono penalizzati
if (!entry.hadRecentInput) {
console.log('Layout Shift detected:', {
score: entry.value.toFixed(4),
time: Math.round(entry.startTime),
sources: entry.sources?.map((source) => ({
element: source.node?.tagName,
elementId: source.node?.id,
previousRect: source.previousRect,
currentRect: source.currentRect,
})),
});
}
}
});
observer.observe({ type: 'layout-shift', buffered: true });
// Calcola il CLS cumulativo (sessione finestra da 5s)
let clsScore = 0;
let sessionValue = 0;
let sessionEntries: PerformanceEntry[] = [];
const clsObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
const firstEntry = sessionEntries[0];
const lastEntry = sessionEntries[sessionEntries.length - 1];
if (
sessionEntries.length === 0 ||
entry.startTime - lastEntry.startTime < 1000 &&
entry.startTime - firstEntry.startTime < 5000
) {
sessionEntries.push(entry);
sessionValue += entry.value;
} else {
clsScore = Math.max(clsScore, sessionValue);
sessionEntries = [entry];
sessionValue = entry.value;
}
}
}
clsScore = Math.max(clsScore, sessionValue);
console.log('Current CLS:', clsScore.toFixed(4));
});
clsObserver.observe({ type: 'layout-shift', buffered: true });
Imagini fără dimensiuni: principalul vinovat
Cel mai frecvent caz de CLS ridicat: imagini fără atribute width
e height. Browserul nu știe cât spațiu să rezerve pentru imagine
înainte de a se încarca, astfel încât textul este redat mai întâi ocupând spațiu,
apoi imaginea împinge totul în jos când ajunge.
/* CSS: l'aspect-ratio e implicito dagli attributi HTML */
img {
width: 100%; /* si adatta al container */
height: auto; /* mantiene l'aspect-ratio automaticamente */
/* Il browser usa width/height HTML per calcolare lo spazio da riservare */
}
/* aspect-ratio CSS: riserva spazio per qualsiasi contenuto */
/* Contenitore immagine con aspect-ratio 16:9 */
.image-container {
width: 100%;
aspect-ratio: 16 / 9; /* Riserva lo spazio prima del caricamento */
background: #f0f0f0; /* Placeholder visivo */
overflow: hidden;
}
.image-container img {
width: 100%;
height: 100%;
object-fit: cover;
}
/* Per contenuto embedded (iframe, video, mappe) */
.video-wrapper {
position: relative;
width: 100%;
aspect-ratio: 16 / 9;
background: #000;
}
.video-wrapper iframe,
.video-wrapper video {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
/* Skeleton screen: placeholder animato che riserva lo spazio corretto */
.skeleton {
aspect-ratio: 16 / 9;
background: linear-gradient(
90deg,
#f0f0f0 25%,
#e0e0e0 50%,
#f0f0f0 75%
);
background-size: 200% 100%;
animation: skeleton-loading 1.5s infinite;
}
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
Font Swap CLS: ajustarea dimensiunii și valorile de rezervă
Când un font web este încărcat și înlocuiește fontul de sistem, textul
poate schimba dimensiunea și poate provoca o schimbare a aspectului. size-adjust,
ascent-override, descent-override e line-gap-override
vă permit să calibrați valorile fonturilor alternative pentru a le reduce sau elimina
această schimbare.
/* Minimizzare il CLS da font swap con @font-face overrides */
/* Passo 1: misura le metriche del tuo web font */
/* Usa: https://screenspan.net/fallback o il Chrome DevTools > Fonts */
/* Passo 2: crea un @font-face per il fallback calibrato */
@font-face {
font-family: 'Inter-Fallback';
src: local('Arial'); /* Font di sistema disponibile ovunque */
/* Valori per avvicinare Arial alle metriche di Inter */
/* (calcolati con fontaine o screenspan.net) */
ascent-override: 90.20%;
descent-override: 22.48%;
line-gap-override: 0%;
size-adjust: 107.40%;
}
/* Passo 3: usa il fallback nella font-family stack */
body {
font-family: 'Inter', 'Inter-Fallback', system-ui, sans-serif;
}
/* Risultato: quando Inter caricherà, il layout quasi non cambierà
perché Arial è stato ridimensionato per avere le stesse metriche */
/* font-display: optional per zero FOIT e CLS minimo */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-400.woff2') format('woff2');
font-weight: 400;
font-display: optional;
/* Se il font non è in cache al primo caricamento:
usa il fallback per sempre (zero swap = zero CLS)
Dal secondo caricamento: font in cache, usato immediatamente */
}
Conținut injectat dinamic
Bannere publicitare, consimțământ pentru cookie-uri, notificări push, conținut încărcat prin preluare: tot conținutul care este injectat în DOM după redarea inițială poate cauzează schimbarea aspectului dacă spațiul nu este rezervat în avans.
/* Riserva spazio per banner pubblicitari e cookie consent */
/* Banner pubblicitari: usa min-height per riservare spazio */
.ad-slot {
min-height: 90px; /* Dimensione standard banner leaderboard */
width: 100%;
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
}
/* Cookie consent banner: posiziona in modo non-intrusivo */
.cookie-consent {
position: fixed; /* fixed non causa layout shift sul contenuto */
bottom: 0;
left: 0;
right: 0;
z-index: 9999;
/* Non usa position: sticky o relative che potrebbero shiftare il contenuto */
}
/* Contenuto lazy-loaded: schermata skeleton con dimensione fissa */
.lazy-content {
min-height: 200px; /* Riserva spazio prima del caricamento */
}
.lazy-content.loaded {
min-height: auto; /* Rimuovi dopo il caricamento se il contenuto è più alto */
}
/* SBAGLIATO: inserire contenuto in alto nella pagina */
/* Evita di aggiungere elementi PRIMA del contenuto gia visibile */
/* CORRETTO: usa position: fixed o bottom per contenuto dinamico */
.notification-bar {
position: fixed; /* Non sposta il contenuto esistente */
top: 0;
left: 0;
right: 0;
}
Animații care provoacă CLS
Nu toate animațiile cauzează CLS. Animațiile pe care le folosesc transform
e opacity Se întâmplă pe firul compozitorului și nu provoacă schimbări de aspect.
Animațiile în schimbare top, left, width,
height, margin, padding ele provoacă reflow
și pot contribui la CLS dacă apar fără interacțiunea utilizatorului.
/* Animazioni CLS-safe: usa solo transform e opacity */
/* SBAGLIATO: anima proprieta che causano reflow */
.slide-in-bad {
animation: slideInBad 0.3s ease;
}
@keyframes slideInBad {
from { top: -100px; } /* causa layout reflow */
to { top: 0; }
}
/* CORRETTO: usa transform (compositor-only, nessun layout reflow) */
.slide-in-good {
animation: slideInGood 0.3s ease;
}
@keyframes slideInGood {
from { transform: translateY(-100px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
/* Accordion / Expand-Collapse: usa max-height con attenzione */
/* max-height puo causare CLS se l'elemento e above-the-fold */
.accordion-content {
overflow: hidden;
/* Usa Grid per animazioni smooth senza max-height */
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.3s ease;
}
.accordion-content.open {
grid-template-rows: 1fr;
}
.accordion-content > div {
overflow: hidden;
}
/* Tooltip e overlay: position absolute/fixed mai relative */
.tooltip {
position: absolute; /* Non sposta il contenuto circostante */
z-index: 1000;
pointer-events: none;
}
/* Usa will-change con parsimonia per ottimizzare animazioni pesanti */
.heavy-animation {
will-change: transform;
/* Solo se l'animazione e effettivamente pesante,
will-change crea un nuovo layer che usa piu memoria */
}
Depanarea CLS cu Chrome DevTools
Chrome DevTools oferă instrumente specifice pentru a identifica cauza exactă a fiecărei schimbări de layout. Cea mai eficientă metodă:
// Step-by-step: debug CLS in Chrome DevTools
// 1. Apri DevTools > Performance
// 2. Clicca "Record" e ricarica la pagina
// 3. Ferma la registrazione dopo il caricamento completo
// 4. Nella timeline, cerca il marker "Layout Shift" (barra rossa)
// 5. Clicca su un Layout Shift entry per vedere:
// - "Sources": quali elementi si sono spostati
// - "Score": il contributo di questo shift al CLS
// - "Had Recent Input": se causato da interazione utente (non conta)
// 6. Usa "Layout Shift Regions" checkbox nella barra superiore
// per visualizzare in overlay gli elementi che si spostano
// Oppure usa questa snippet nella Console:
document.addEventListener('DOMContentLoaded', () => {
const obs = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput && entry.value > 0) {
const elements = entry.sources
?.map((s) => s.node)
.filter(Boolean) ?? [];
// Evidenzia in rosso gli elementi che causano shift
elements.forEach((el) => {
if (el instanceof HTMLElement) {
el.style.outline = '3px solid red';
el.title = `CLS: ${entry.value.toFixed(4)}`;
setTimeout(() => {
el.style.outline = '';
}, 3000);
}
});
console.warn('CLS shift:', entry.value.toFixed(4), elements);
}
}
});
obs.observe({ type: 'layout-shift', buffered: true });
});
Lista de verificare CLS: Schimbarea aspectului zero
- Toate imaginile au atribute de lățime și înălțime (sau raportul de aspect prin CSS)
- Fonturile folosesc font-display: swap sau opțional cu fallback calibrat
- Anunțurile banner au spațiu rezervat cu înălțime minimă
- Poziția de utilizare a consimțământului cookie-urilor și a notificărilor: fixă
- Animațiile folosesc numai transformare și opacitate (nu sus/stânga/lățime)
- Conținutul încărcat lenev are un substituent de dimensiune echivalentă
- Verificați CLS în Chrome DevTools pe mobil (emulație 4G)
Concluzii
CLS este cea mai controlabilă metrică Core Web Vitals: spre deosebire de LCP (care depinde de rețea și server) și INP (care depinde de complexitatea JavaScript), CLS depinde aproape în întregime de opțiunile CSS și HTML. Cu dimensiuni explicit pe imagini, afișare optimizată a fonturilor și conținut dinamic gestionat cu pozitia fixa, poti aduce CLS-ul la zero pe orice tip de site.







