CLS-preventie: visuele stabiliteit en foutopsporing in lay-outverschuiving
Je leest een artikel. U staat op het punt op een interessante link te klikken. Plotseling de inhoud klikt 200 pixels naar beneden: er verschijnt een reclamebanner hierboven. Je hebt op de verkeerde link geklikt. Dit fenomeen – de lay-out verschuiving - en precies wat de Cumulatieve lay-outverschuiving (CLS) meeteenheid. Google beschouwt het als een van de drie belangrijkste Core Web Vitals voor gebruikerservaring.
Een CLS lager dan 0,1 wordt als "goed" beschouwd; tussen 0,1 en 0,25 "om te verbeteren"; ruim 0,25 "schaars". Het goede nieuws: in tegenstelling tot LCP (dat netwerkoptimalisaties vereist). en server), CLS is vrijwel volledig afhankelijk van CSS en HTML. Met de juiste technieken je kunt de CLS naar nul brengen.
Wat je gaat leren
- Hoe de browser CLS berekent: impactfractie, afstandsfractie, score
- Identificeer de oorzaken van CLS met Layout Shift Regions in Chrome DevTools
- Grootteloze afbeeldingen: de grootste boosdoener
- aspect-ratio CSS: reserveer ruimte voordat de afbeelding wordt geladen
- CLS-lettertypewissel: weergave van lettertypen en aanpassing van de grootte voor reservestatistieken
- Dynamisch geïnjecteerde inhoud: banneradvertenties, toestemming voor cookies, skeletschermen
- Animaties die CLS veroorzaken: transformeren vs. boven/links
Hoe CLS wordt berekend
De CLS meet niet hoeveel diensten er plaatsvinden, maar hoeveel deze de gebruiker ‘storen’. Elke lay-outverschuiving levert een score op die wordt berekend als:
lay-outverschuivingsscore = impactfractie × afstandsfractie
L'impactfractie en het percentage van de viewport dat wordt "geïmpacteerd" van de dienst (gebied bezet vóór + gebied bezet daarna). Daar afstandsfractie en de maximale afstand die een getroffen item heeft afgelegd, als een fractie van de viewportgrootte. De uiteindelijke CLS is de som van alle diensten vond plaats in sessievensters van 5 seconden, waarbij het maximum werd genomen.
// 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 });
Dimensieloze afbeeldingen: de hoofdschuldige
Het meest voorkomende geval van hoge CLS: afbeeldingen zonder attributen width
e height. De browser weet niet hoeveel ruimte hij voor de afbeelding moet reserveren
voordat het wordt geladen, zodat de tekst eerst wordt weergegeven en de ruimte in beslag neemt,
dan duwt het beeld alles naar beneden wanneer het arriveert.
/* 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: grootte aanpassen en terugvalstatistieken
Wanneer een weblettertype wordt geladen en het systeemlettertype vervangt, wordt de tekst
kan van formaat veranderen en een lay-outverschuiving veroorzaken. size-adjust,
ascent-override, descent-override e line-gap-override
kunt u de fallback-lettertypestatistieken kalibreren om deze te verminderen of te elimineren
deze verschuiving.
/* 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 */
}
Dynamisch geïnjecteerde inhoud
Advertentiebanners, toestemming voor cookies, pushmeldingen, inhoud geladen via ophalen: alle inhoud die na de eerste weergave in de DOM wordt geïnjecteerd, kan dat wel indelingsverschuiving veroorzaken als er niet vooraf ruimte wordt gereserveerd.
/* 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;
}
Animaties die CLS veroorzaken
Niet alle animaties veroorzaken CLS. De animaties die ze gebruiken transform
e opacity Ze gebeuren in de componistthread en veroorzaken geen lay-outverschuivingen.
De wisselende animaties top, left, width,
height, margin, padding ze veroorzaken terugvloeiing
en kunnen bijdragen aan de CLS als ze plaatsvinden zonder gebruikersinteractie.
/* 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 */
}
Foutopsporing in CLS met Chrome DevTools
Chrome DevTools biedt specifieke tools om de exacte oorzaak te achterhalen van elke lay-outverschuiving. De meest effectieve methode:
// 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 });
});
CLS-checklist: nul lay-outverschuiving
- Alle afbeeldingen hebben breedte- en hoogtekenmerken (of beeldverhouding via CSS)
- Lettertypen gebruiken lettertypeweergave: wisselen of optioneel met gekalibreerde fallback
- Voor banneradvertenties is ruimte gereserveerd met een minimale hoogte
- Toestemming voor cookies en meldingen gebruiken positie: vast
- Animaties gebruiken alleen transformatie en dekking (niet boven/links/breedte)
- Lazy-loaded inhoud heeft een tijdelijke aanduiding van gelijke grootte
- Controleer CLS in Chrome DevTools op mobiel (4G-emulatie)
Conclusies
CLS is de meest controleerbare Core Web Vitals-metriek: in tegenstelling tot LCP (die afhangt van het netwerk en de server) en de INP (die afhangt van de complexiteit van het JavaScript), CLS is vrijwel volledig afhankelijk van CSS- en HTML-keuzes. Met afmetingen expliciet op afbeeldingen, geoptimaliseerde weergave van lettertypen en beheerde dynamische inhoud met een vaste positie kunt u de CLS op elk type terrein op nul zetten.







