Prevence CLS: Vizuální stabilita a ladění změny rozložení
Čtete článek. Chystáte se kliknout na zajímavý odkaz. Najednou obsah se stlačí o 200 pixelů: zobrazí se reklamní banner výše. Klikli jste na špatný odkaz. Tento fenomén — posun rozložení - a přesně co Kumulativní změna rozložení (CLS) opatření. Google jej považuje za jeden ze tří nejdůležitějších Core Web Vitals pro uživatelský zážitek.
CLS pod 0,1 je považováno za „dobré“; mezi 0,1 a 0,25 "pro zlepšení"; přes 0,25 „nedostatečné“. Dobrá zpráva: na rozdíl od LCP (které vyžaduje optimalizaci sítě a server), CLS závisí téměř výhradně na CSS a HTML. Se správnými technikami můžete snížit CLS na nulu.
Co se naučíte
- Jak prohlížeč počítá CLS: zlomek dopadu, zlomek vzdálenosti, skóre
- Identifikujte příčiny CLS pomocí Layout Shift Regions v Chrome DevTools
- Bezrozměrné obrázky: viník číslo jedna
- CSS poměr stran: rezervuje místo před načtením obrázku
- Výměna písma CLS: zobrazení písma a úprava velikosti pro záložní metriky
- Dynamicky vkládaný obsah: bannerové reklamy, souhlas se soubory cookie, základní obrazovky
- Animace, které způsobují CLS: transformace vs. nahoře/vlevo
Jak se vypočítává CLS
CLS neměří, k kolika posunům dochází, ale jak moc „ruší“ uživatele. Každý posun rozvržení vytváří skóre vypočítané takto:
skóre posunu rozložení = zlomek dopadu × zlomek vzdálenosti
L'nárazový zlomek a procento zobrazované oblasti, která je "ovlivněna" ze směny (plocha obsazená před + plocha obsazená po). Tam zlomek vzdálenosti a maximální vzdálenost, kterou urazí jakákoliv zasažená položka, jako zlomek velikosti výřezu. Konečný CLS je součtem všech směn došlo v oknech relace 5 sekund, přičemž 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 });
Bezrozměrné obrázky: Hlavní viník
Nejběžnější případ vysokého CLS: obrázky bez atributů width
e height. Prohlížeč neví, kolik místa má pro obrázek vyhradit
než se načte, takže text se vykreslí jako první a zabere místo,
pak obraz při příchodu stlačí vše dolů.
/* 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: úprava velikosti a záložní metriky
Když je načteno webové písmo a nahradí systémové písmo, text
může změnit velikost a způsobit změnu rozložení. size-adjust,
ascent-override, descent-override e line-gap-override
vám umožní kalibrovat záložní metriky písem pro snížení nebo odstranění
tento posun.
/* 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 */
}
Dynamicky vkládaný obsah
Reklamní bannery, souhlas se soubory cookie, oznámení push, obsah načtený pomocí načtení: veškerý obsah, který je vložen do DOM po počátečním vykreslení způsobit posun rozvržení, pokud není prostor předem rezervován.
/* 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;
}
Animace, které způsobují CLS
Ne všechny animace způsobují CLS. Animace, které používají transform
e opacity Dochází k nim ve vláknu skladatele a nezpůsobují posuny rozvržení.
Měnící se animace top, left, width,
height, margin, padding způsobují reflow
a mohou přispět k CLS, pokud k nim dojde bez interakce uživatele.
/* 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 */
}
Ladění CLS pomocí Chrome DevTools
Chrome DevTools nabízí specifické nástroje k identifikaci přesné příčiny každé směny rozložení. The most effective method:
// 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 });
});
Kontrolní seznam CLS: Zero Layout Shift
- Všechny obrázky mají atributy width a height (nebo poměr stran pomocí CSS)
- Písma používají zobrazení písma: swap nebo volitelně s kalibrovanou rezervou
- Bannerové reklamy mají vyhrazený prostor s minimální výškou
- Umístění souhlasu s používáním souborů cookie a oznámení: opraveno
- Animace používají pouze transformaci a neprůhlednost (nikoli nahoře/vlevo/šířka)
- Líně načtený obsah má zástupný symbol stejné velikosti
- Zkontrolujte CLS v Chrome DevTools na mobilu (emulace 4G)
Závěry
CLS je nejkontrolovatelnější metrika Core Web Vitals: na rozdíl od LCP (který závisí na síti a serveru) a INP (který závisí na složitosti JavaScript), CLS závisí téměř výhradně na volbách CSS a HTML. S rozměry explicitní na obrázcích, optimalizované zobrazení písem a spravovaný dynamický obsah s pevnou pozicí můžete vynulovat CLS na jakémkoli typu webu.







