Skip to content

CLS 累積版位偏移:版面跳動的六種成因與修法

CLS 累積版位偏移:版面跳動的成因與修法

CLS(Cumulative Layout Shift,累積版位偏移)回答的是:「畫面怎麼一直跳?」

評級門檻
良好≤ 0.1
待改善0.1 - 0.25
不佳> 0.25

這是三個指標中最該優先修的

成本最低、改動最明確。多數網站的問題只是 圖片沒標寬高,補上之後分數立刻改善,而且不會影響其他部分。

LCP 要處理圖片與資源優先權,INP 常要調整架構,這一項通常一個下午就修完了。

只算「非預期」的位移

使用者操作造成的位移不算

使用者互動之後 500 毫秒內 發生的位移會被排除,那是預期中的變化,例如:點開收合區塊、展開選單。

所以互動觸發的展開不必擔心。真正計入的是 使用者什麼都沒做,畫面自己跳 的情況。

六種成因與修法

1. 圖片沒標寬高

這是最常見的一種,也最容易修。

html
<!-- 圖片載完才知道多高,下方內容被往下推 -->
<img src="/photo.webp" alt="說明文字">

<!-- 事先預留空間 -->
<img src="/photo.webp" alt="說明文字" width="1200" height="800">

標上寬高不等於固定尺寸

實際顯示大小仍由樣式決定。這兩個屬性提供的是 寬高比,讓瀏覽器算得出該預留多高。

現代瀏覽器會從 widthheight 自動推導出 aspect-ratio,所以搭配這樣的樣式就能維持響應式:

css
img {
  max-width: 100%;
  height: auto;
}

尺寸不固定的情況用 aspect-ratio

css
.thumbnail {
  aspect-ratio: 16 / 9;
  width: 100%;
  object-fit: cover;
}

2. iframe 與嵌入內容沒預留尺寸

html
<!-- 載入前高度是 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>
css
.video-wrapper {
  aspect-ratio: 16 / 9;
}

.video-wrapper iframe {
  width: 100%;
  height: 100%;
  border: 0;
}

內置頁框 的其他注意事項見那一篇。

3. 動態插入的內容

橫幅、公告、Cookie 提示這類「載入後才插到頁面上方」的元素:

js
// 插在內容上方,把整頁往下推
document.body.prepend(banner);
修法說明
預留版位用容器固定高度,內容載入後填進去
改用覆蓋層position: fixed 浮在內容上,不佔版面
插在下方插到可視範圍之外的位置
css
/* 預留版位:無論公告有沒有載入,高度都一樣 */
.notice-slot {
  min-height: 48px;
}
css
/* 或改用覆蓋層,完全不影響版面 */
.cookie-banner {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
}

4. 字型替換

備援字型與目標字型的 字寬與行高不同,換字的瞬間文字重新排列。

css
/* 調整備援字型的度量,讓它接近目標字型 */
@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

css
@font-face {
  font-family: "MyFont";
  src: url("/fonts/myfont.woff2") format("woff2");
  font-display: optional;
}

optional 表示「如果字型在極短時間內載入完就用它,否則這次造訪就一直用備援字型」,完全不會有換字造成的位移

代價是使用者第一次造訪可能看不到您的品牌字型。權衡見 字型顯示 font-display

5. 廣告版位

廣告的高度常由廣告伺服器決定,載入前不知道多高。

css
/* 用最常出現的尺寸預留最小高度 */
.ad-slot {
  min-height: 250px;
}

/* 行動裝置的版位通常較小 */
@media (max-width: 600px) {
  .ad-slot {
    min-height: 100px;
  }
}

沒填滿的版位不要塌陷

廣告沒有回填時,版位如果塌陷成 0 高度,一樣是一次位移。保持 min-height 讓它維持空白比較好。

6. 動畫用錯屬性

這些屬性會觸發版面重算

css
/* 錯誤:改高度會推移下方所有內容 */
.panel {
  transition: height 0.3s;
}

/* 錯誤:改邊界同樣影響版面 */
.card:hover {
  transition: margin-top 0.2s;
}
會觸發版面重算交由合成層處理
widthheighttransform
marginpaddingopacity
topleftrightbottomfilter
font-size
css
/* 正確:用 transform 移動,不影響版面 */
.card {
  transition: transform 0.2s;
}

.card:hover {
  transform: translateY(-4px);
}

展開收合的動畫

高度動畫是常見需求。可以用 max-heightoverflow: hidden(仍會影響版面但可控),或用 scale 搭配 transform-origin

現代瀏覽器也支援 interpolate-size: allow-keywordsheight: auto 可以參與轉場。相關概念見 轉場 transition2D 變形

怎麼找出位移的元素

js
// 印出每一次位移的來源元素
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 的圖片語法本身不帶尺寸,所以這個站在每張圖後面補上屬性:

markdown
![說明文字](/images/seo/core-web-vitals-cls-banner.svg){ width=1200 height=400 fetchpriority="high" }

LinkCard 元件也在 <img> 上寫死了尺寸:

html
<img :src="image" :alt="imgAlt" width="1200" height="400" loading="lazy">

搭配樣式的 aspect-ratio 確保版位在圖片載入前就已經定下來:

css
.link-card__media {
  aspect-ratio: 3 / 1;
}

排查流程

  1. 開 DevTools 的 Rendering,勾選 Layout Shift Regions:重新載入,看哪裡閃藍色。
  2. 檢查那些元素有沒有標寬高:圖片、iframe、嵌入內容。
  3. 看有沒有動態插入的元素:公告、廣告、Cookie 提示。
  4. 檢查字型策略:有網頁字型的話,換字時文字會不會重排。
  5. 檢查動畫用的屬性:有沒有在動 heightmargintop
  6. 用真實數據驗收:見 指標量測與 CrUX 數據

檢查清單

項目標準
CLS 在 0.1 以內(第 75 百分位)目標
所有 <img>widthheight必備
<iframe> 與嵌入內容有預留比例必備
廣告版位有 min-height必備
動態插入的內容有預留版位或用覆蓋層必備
字型有處理度量落差或用 optional建議
動畫只用 transformopacity建議
用 Layout Shift Regions 檢查過建議

常見問題

為什麼建議先修這個指標?

因為成本最低而且改動明確。多數網站的問題只是圖片沒標寬高,補上之後分數立刻改善,而且不會影響其他部分。另外兩個指標往往需要調整架構或處理第三方資源。

標上寬高就等於固定圖片尺寸嗎?

不是。實際顯示大小仍由樣式決定,這兩個屬性提供的是寬高比,讓瀏覽器在圖片還沒下載完時就算得出該預留多高。搭配 max-width: 100%height: auto 即可維持響應式。

使用者操作造成的位移也算嗎?

不算。使用者互動之後 500 毫秒內發生的位移會被排除,因為那是預期中的變化,例如:點開收合區塊。所以互動觸發的展開不必擔心,真正計入的是使用者沒做任何事卻自己跳動的情況。

字型替換為什麼會造成位移?

備援字型與目標字型的字寬與行高不同,換字的瞬間文字會重新排列,把下方內容推移。可以用 size-adjust 這類調整備援字型度量的屬性讓兩者對齊,減少換字時的落差。

做動畫要注意什麼?

改寬高、邊界與定位屬性會觸發版面重算並計入位移;改 transformopacity 則交由合成層處理,不影響版面。所以動畫一律優先用變形與不透明度這兩類屬性。

延伸閱讀

參考資料:web.dev:Cumulative Layout Shift (CLS)