CLS 防止: 視覚的な安定性とレイアウト シフトのデバッグ
あなたは記事を読んでいます。あなたは興味深いリンクをクリックしようとしています。突然 コンテンツが 200 ピクセル下にクリックされると、広告バナーが表示されます 上。間違ったリンクをクリックしました。この現象は、 レイアウト変更 — そしてまさにその内容は、 累積レイアウト シフト (CLS) 測定。 Google は、これをユーザー エクスペリエンスにとって最も重要な 3 つのコア ウェブ バイタルの 1 つと考えています。
0.1 未満の CLS は「良好」とみなされます。 0.1 ~ 0.25 の間で「改善する」。 0.25以上 「希少」。良いニュース: LCP とは異なります (ネットワークの最適化が必要です) およびサーバー)、CLS はほぼ完全に CSS と HTML に依存しています。適切なテクニックを使えば CLS をゼロにすることができます。
何を学ぶか
- ブラウザによる CLS の計算方法: 衝撃率、距離率、スコア
- Chrome DevTools のレイアウト シフト領域による CLS の原因を特定する
- サイズのない画像: 最大の犯人
- アスペクト比 CSS: 画像をロードする前にスペースを確保する
- CLS フォント スワップ: フォールバック メトリックのフォント表示とサイズ調整
- 動的に挿入されるコンテンツ: バナー広告、Cookie 同意、スケルトン画面
- 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: サイズ調整とフォールバック メトリック
Web フォントがロードされてシステム フォントと置き換わると、テキスト
サイズが変更され、レイアウトが変わる可能性があります。 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 */
}
動的に挿入されるコンテンツ
広告バナー、Cookie 同意、プッシュ通知、フェッチ経由で読み込まれるコンテンツ: 最初のレンダリング後に 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 が発生すると、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 によるアスペクト比) があります。
- フォントは font-display: swap を使用するか、調整されたフォールバックを使用してオプションで使用します。
- バナー広告には最小高さで予約されたスペースがあります
- Cookieの同意と通知の使用位置: 固定
- アニメーションは変換と不透明度のみを使用します (上/左/幅は使用しません)。
- 遅延ロードされたコンテンツには同等のサイズのプレースホルダーがあります
- モバイル上の Chrome DevTools で CLS を確認する(4G エミュレーション)
結論
CLS は、LCP とは異なり、最も制御可能な Core Web Vitals 指標です (ネットワークとサーバーに依存します) および INP (ネットワークとサーバーの複雑さに依存します) JavaScript)、CLS はほぼ完全に CSS と HTML の選択に依存します。寸法あり 明示的な画像、最適化されたフォント表示、および管理された動的コンテンツ 位置を固定すると、どのタイプのサイトでも CLS をゼロにすることができます。







