CLS 累積版位偏移:版面跳動的六種成因與修法
CLS(Cumulative Layout Shift,累積版位偏移)回答的是:「畫面怎麼一直跳?」
| 評級 | 門檻 |
|---|---|
| 良好 | ≤ 0.1 |
| 待改善 | 0.1 - 0.25 |
| 不佳 | > 0.25 |
這是三個指標中最該優先修的
成本最低、改動最明確。多數網站的問題只是 圖片沒標寬高,補上之後分數立刻改善,而且不會影響其他部分。
只算「非預期」的位移
使用者操作造成的位移不算
使用者互動之後 500 毫秒內 發生的位移會被排除,那是預期中的變化,例如:點開收合區塊、展開選單。
所以互動觸發的展開不必擔心。真正計入的是 使用者什麼都沒做,畫面自己跳 的情況。
六種成因與修法
1. 圖片沒標寬高
這是最常見的一種,也最容易修。
<!-- 圖片載完才知道多高,下方內容被往下推 -->
<img src="/photo.webp" alt="說明文字">
<!-- 事先預留空間 -->
<img src="/photo.webp" alt="說明文字" width="1200" height="800">標上寬高不等於固定尺寸
實際顯示大小仍由樣式決定。這兩個屬性提供的是 寬高比,讓瀏覽器算得出該預留多高。
現代瀏覽器會從 width 與 height 自動推導出 aspect-ratio,所以搭配這樣的樣式就能維持響應式:
img {
max-width: 100%;
height: auto;
}尺寸不固定的情況用 aspect-ratio:
.thumbnail {
aspect-ratio: 16 / 9;
width: 100%;
object-fit: cover;
}2. iframe 與嵌入內容沒預留尺寸
<!-- 載入前高度是 0,載入後撐開整個版面 -->
<iframe src="https://www.youtube.com/embed/xxx"></iframe>
<!-- 用容器預留比例 -->
<div class="video-wrapper">
<iframe src="https://www.youtube.com/embed/xxx" title="影片說明"></iframe>
</div>.video-wrapper {
aspect-ratio: 16 / 9;
}
.video-wrapper iframe {
width: 100%;
height: 100%;
border: 0;
}內置頁框 的其他注意事項見那一篇。
3. 動態插入的內容
橫幅、公告、Cookie 提示這類「載入後才插到頁面上方」的元素:
// 插在內容上方,把整頁往下推
document.body.prepend(banner);| 修法 | 說明 |
|---|---|
| 預留版位 | 用容器固定高度,內容載入後填進去 |
| 改用覆蓋層 | 用 position: fixed 浮在內容上,不佔版面 |
| 插在下方 | 插到可視範圍之外的位置 |
/* 預留版位:無論公告有沒有載入,高度都一樣 */
.notice-slot {
min-height: 48px;
}/* 或改用覆蓋層,完全不影響版面 */
.cookie-banner {
position: fixed;
bottom: 0;
left: 0;
right: 0;
}4. 字型替換
備援字型與目標字型的 字寬與行高不同,換字的瞬間文字重新排列。
/* 調整備援字型的度量,讓它接近目標字型 */
@font-face {
font-family: "FallbackAdjusted";
src: local("Microsoft JhengHei");
size-adjust: 97%;
ascent-override: 88%;
descent-override: 12%;
line-gap-override: 0%;
}
body {
font-family: "MyFont", "FallbackAdjusted", sans-serif;
}| 屬性 | 調整什麼 |
|---|---|
size-adjust | 整體字級縮放比例 |
ascent-override | 基線以上的高度 |
descent-override | 基線以下的高度 |
line-gap-override | 行距 |
另一個選擇是 font-display: optional
@font-face {
font-family: "MyFont";
src: url("/fonts/myfont.woff2") format("woff2");
font-display: optional;
}optional 表示「如果字型在極短時間內載入完就用它,否則這次造訪就一直用備援字型」,完全不會有換字造成的位移。
代價是使用者第一次造訪可能看不到您的品牌字型。權衡見 字型顯示 font-display 。
5. 廣告版位
廣告的高度常由廣告伺服器決定,載入前不知道多高。
/* 用最常出現的尺寸預留最小高度 */
.ad-slot {
min-height: 250px;
}
/* 行動裝置的版位通常較小 */
@media (max-width: 600px) {
.ad-slot {
min-height: 100px;
}
}沒填滿的版位不要塌陷
廣告沒有回填時,版位如果塌陷成 0 高度,一樣是一次位移。保持 min-height 讓它維持空白比較好。
6. 動畫用錯屬性
這些屬性會觸發版面重算
/* 錯誤:改高度會推移下方所有內容 */
.panel {
transition: height 0.3s;
}
/* 錯誤:改邊界同樣影響版面 */
.card:hover {
transition: margin-top 0.2s;
}| 會觸發版面重算 | 交由合成層處理 |
|---|---|
width、height | transform |
margin、padding | opacity |
top、left、right、bottom | filter |
font-size |
/* 正確:用 transform 移動,不影響版面 */
.card {
transition: transform 0.2s;
}
.card:hover {
transform: translateY(-4px);
}展開收合的動畫
高度動畫是常見需求。可以用 max-height 加 overflow: hidden(仍會影響版面但可控),或用 scale 搭配 transform-origin。
現代瀏覽器也支援 interpolate-size: allow-keywords 讓 height: auto 可以參與轉場。相關概念見 轉場 transition 與 2D 變形 。
怎麼找出位移的元素
// 印出每一次位移的來源元素
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.hadRecentInput) continue; // 排除使用者操作造成的
console.log('位移量:', entry.value);
for (const source of entry.sources ?? []) {
console.log(' 來源元素:', source.node);
}
}
}).observe({ type: 'layout-shift', buffered: true });| 工具 | 怎麼看 |
|---|---|
| 上面的觀察器 | 直接印出 source.node,最精確 |
| Lighthouse | 「Avoid large layout shifts」列出貢獻最大的元素 |
| DevTools 的 Rendering 面板 | 勾選「Layout Shift Regions」,位移區域會閃藍色 |
Layout Shift Regions 最直觀
DevTools 的 Rendering 面板打開這個選項後重新載入頁面,發生位移的區域會閃一下藍色框,用肉眼就看得出哪裡在跳。
這個站的做法
Markdown 的圖片語法本身不帶尺寸,所以這個站在每張圖後面補上屬性:
{ width=1200 height=400 fetchpriority="high" }LinkCard 元件也在 <img> 上寫死了尺寸:
<img :src="image" :alt="imgAlt" width="1200" height="400" loading="lazy">搭配樣式的 aspect-ratio 確保版位在圖片載入前就已經定下來:
.link-card__media {
aspect-ratio: 3 / 1;
}排查流程
- 開 DevTools 的 Rendering,勾選 Layout Shift Regions:重新載入,看哪裡閃藍色。
- 檢查那些元素有沒有標寬高:圖片、iframe、嵌入內容。
- 看有沒有動態插入的元素:公告、廣告、Cookie 提示。
- 檢查字型策略:有網頁字型的話,換字時文字會不會重排。
- 檢查動畫用的屬性:有沒有在動
height、margin、top。 - 用真實數據驗收:見 指標量測與 CrUX 數據 。
檢查清單
| 項目 | 標準 |
|---|---|
| CLS 在 0.1 以內(第 75 百分位) | 目標 |
所有 <img> 有 width 與 height | 必備 |
<iframe> 與嵌入內容有預留比例 | 必備 |
廣告版位有 min-height | 必備 |
| 動態插入的內容有預留版位或用覆蓋層 | 必備 |
字型有處理度量落差或用 optional | 建議 |
動畫只用 transform 與 opacity | 建議 |
| 用 Layout Shift Regions 檢查過 | 建議 |
常見問題
為什麼建議先修這個指標?
因為成本最低而且改動明確。多數網站的問題只是圖片沒標寬高,補上之後分數立刻改善,而且不會影響其他部分。另外兩個指標往往需要調整架構或處理第三方資源。
標上寬高就等於固定圖片尺寸嗎?
不是。實際顯示大小仍由樣式決定,這兩個屬性提供的是寬高比,讓瀏覽器在圖片還沒下載完時就算得出該預留多高。搭配 max-width: 100% 與 height: auto 即可維持響應式。
使用者操作造成的位移也算嗎?
不算。使用者互動之後 500 毫秒內發生的位移會被排除,因為那是預期中的變化,例如:點開收合區塊。所以互動觸發的展開不必擔心,真正計入的是使用者沒做任何事卻自己跳動的情況。
字型替換為什麼會造成位移?
備援字型與目標字型的字寬與行高不同,換字的瞬間文字會重新排列,把下方內容推移。可以用 size-adjust 這類調整備援字型度量的屬性讓兩者對齊,減少換字時的落差。
做動畫要注意什麼?
改寬高、邊界與定位屬性會觸發版面重算並計入位移;改 transform 與 opacity 則交由合成層處理,不影響版面。所以動畫一律優先用變形與不透明度這兩類屬性。