CLS 예방: 시각적 안정성 및 레이아웃 이동 디버깅
당신은 기사를 읽고 있습니다. 흥미로운 링크를 클릭하려고 합니다. 갑자기 콘텐츠가 200픽셀 아래로 클릭되면 광고 배너가 나타납니다. 위. 잘못된 링크를 클릭하셨습니다. 이 현상은 - 레이아웃 변화 — 그리고 정확히 무엇을 CLS(누적 레이아웃 변경) 측정하다. Google은 이를 사용자 경험에 있어 가장 중요한 세 가지 핵심 웹 바이탈 중 하나로 간주합니다.
0.1 미만의 CLS는 "양호"로 간주됩니다. 0.1에서 0.25 사이 "개선하다"; 0.25 이상 "희소하다". 좋은 소식: LCP(네트워크 최적화가 필요함)와는 다릅니다. 서버), CLS는 거의 전적으로 CSS와 HTML에 의존합니다. 올바른 기술로 CLS를 0으로 만들 수 있습니다.
무엇을 배울 것인가
- 브라우저가 CLS를 계산하는 방법: 영향 비율, 거리 비율, 점수
- Chrome DevTools의 레이아웃 전환 영역을 사용하여 CLS의 원인 식별
- 크기가 없는 이미지: 가장 큰 원인
- 종횡비 CSS: 이미지가 로드되기 전에 공간을 예약합니다.
- CLS 글꼴 교체: 대체 측정항목에 대한 글꼴 표시 및 크기 조정
- 동적으로 삽입된 콘텐츠: 배너 광고, 쿠키 동의, 스켈레톤 화면
- CLS를 유발하는 애니메이션: 변환과 위쪽/왼쪽
CLS 계산 방법
CLS는 교대가 발생하는 횟수가 아니라 사용자를 얼마나 "방해"하는지 측정합니다. 각 레이아웃 변경은 다음과 같이 계산된 점수를 생성합니다.
레이아웃 변화 점수 = 영향 비율 × 거리 비율
L'영향 비율 "영향을 받은" 뷰포트의 비율 교대 근무에서 (이전 점유 영역 + 이후 점유 영역). 거기 거리 비율 영향을 받은 품목이 이동한 최대 거리(분수) 뷰포트 크기. 최종 CLS는 모든 교대근무의 합입니다. 5초의 세션 창에서 발생하여 최대값을 차지했습니다.
// 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 });
무차원 이미지: 주범
높은 CLS의 가장 일반적인 경우: 속성이 없는 이미지 width
e height. 브라우저는 이미지를 위해 얼마나 많은 공간을 예약해야 하는지 모릅니다.
로드되기 전에 텍스트가 먼저 렌더링되어 공간을 차지합니다.
그런 다음 이미지가 도착하면 모든 것을 아래로 밀어냅니다.
/* 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; }
}
글꼴 교체 CLS: 크기 조정 및 대체 측정항목
웹 글꼴이 로드되어 시스템 글꼴을 대체하면 텍스트가
크기가 변경되고 레이아웃이 변경될 수 있습니다. size-adjust,
ascent-override, descent-override e line-gap-override
대체 글꼴 측정항목을 보정하여 줄이거나 제거할 수 있습니다.
이번 교대.
/* 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 */
}
동적으로 주입된 콘텐츠
광고 배너, 쿠키 동의, 푸시 알림, 가져오기를 통해 로드된 콘텐츠: 초기 렌더링 이후 DOM에 주입된 모든 콘텐츠는 공간을 미리 예약하지 않으면 레이아웃이 변경됩니다.
/* 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;
}
CLS를 유발하는 애니메이션
모든 애니메이션이 CLS를 유발하는 것은 아닙니다. 그들이 사용하는 애니메이션 transform
e opacity 이는 작성기 스레드에서 발생하며 레이아웃 변경을 일으키지 않습니다.
변화하는 애니메이션 top, left, width,
height, margin, padding 리플로우를 유발합니다
사용자 상호 작용 없이 발생하는 경우 CLS에 기여할 수 있습니다.
/* 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 */
}
Chrome DevTools를 사용하여 CLS 디버깅
Chrome DevTools는 정확한 원인을 식별하는 특정 도구를 제공합니다. 각 레이아웃 변경의 가장 효과적인 방법:
// 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 체크리스트: 제로 레이아웃 전환
- 모든 이미지에는 너비 및 높이 속성(또는 CSS를 통한 종횡비)이 있습니다.
- 글꼴은 글꼴 표시를 사용합니다. 보정된 폴백으로 교체 또는 선택 사항
- 배너 광고에는 최소 높이로 예약된 공간이 있습니다.
- 쿠키 동의 및 알림 사용 위치 : 고정
- 애니메이션은 변환 및 불투명도만 사용합니다(상단/왼쪽/너비 제외).
- 지연 로드된 콘텐츠에는 동일한 크기의 자리 표시자가 있습니다.
- 모바일의 Chrome DevTools에서 CLS 확인(4G 에뮬레이션)
결론
CLS는 LCP와 달리 가장 제어하기 쉬운 핵심 웹 바이탈 지표입니다. (네트워크와 서버에 따라 다름) 및 INP(네트워크 및 서버의 복잡성에 따라 다름) JavaScript), CLS는 거의 전적으로 CSS 및 HTML 선택에 의존합니다. 치수 포함 이미지에 명시적 표시, 최적화된 글꼴 표시 및 관리되는 동적 콘텐츠 위치가 고정되면 모든 유형의 사이트에서 CLS를 0으로 만들 수 있습니다.







