上週有個電商客戶說他的 Core Web Vitals 一直亮紅燈,我去看 GSC,問題出在 CLS,分數 0.32。開 PSI 跑一下,Lighthouse 診斷項目列出兩個元素:商品圖片和頂部廣告橫幅。再開 DevTools Performance 錄一次,找到廣告 div 在頁面載入 800ms 後才插入,直接把整個商品列表往下推了 180px。這是 CLS 最典型的症狀,原因很確定,修法也很清楚,但問題是很多人根本不知道 CLS 是什麼,更別說知道去哪找問題元素。
這篇文章用「症狀 → 原因 → 診斷 → 修復 → 驗證」這個流程把 CLS 拆清楚。如果你的 GSC 顯示 CLS 不良,或 PageSpeed Insights 分數一直過不了,讀完這篇你應該知道下一步要做什麼。
CLS 是什麼,為什麼 Google 要測量版面穩定性
CLS(Cumulative Layout Shift,累計版面配置位移)是 Core Web Vitals 三大指標之一,衡量的是頁面載入過程中,可見元素發生非預期位移的程度。分數越高,代表頁面畫面越不穩定,使用者越容易遇到「想點這個,結果點到那個」的狀況。
Google 在 2020 年把 Core Web Vitals 納入排名訊號,其中 LCP 衡量載入速度,INP 衡量互動反應,CLS 則衡量視覺穩定性。三個指標各自獨立,CLS 高不代表速度慢,速度快也不代表 CLS 就好。
使用者遇到高 CLS 的實際感受通常有三種:讀文章讀到一半,圖片載入把段落往下推,找不到剛剛在哪;準備點「送出訂單」,廣告突然插進來,點到廣告連結;按表單的「下一步」,輸入框位置移動,按到錯誤的按鈕。這些都是真實案例。
CLS 的合格標準
| 分數範圍 | 評等 | 代表什麼 |
|---|---|---|
| 低於 0.1 | 良好(Good) | 頁面視覺穩定,使用者幾乎不會察覺位移 |
| 0.1 到 0.25 | 需要改善(Needs Improvement) | 有明顯位移,部分使用者會受影響 |
| 高於 0.25 | 不良(Poor) | 位移嚴重,使用者體驗明顯受損,應優先排查 |
Google 以第 75 個百分位數作為評估門檻,也就是說,該頁面 75% 的真實使用者體驗都應達到「良好」標準,才算通過 CLS 評核。
哪些位移不算在 CLS 裡
並非所有看得到的畫面變動都計入 CLS。Google 把位移分成「預期位移」和「非預期位移」兩類,只有非預期位移才算分。
使用者主動觸發的位移,在互動後 500 毫秒內發生的版面變動,不計入 CLS。舉例:使用者點了「展開詳細說明」,內容區塊撐開,把下方內容往下推,這個不算;使用者點了「加入購物車」,按鈕暫時換成 loading spinner,頁面稍微跳動,不算;使用者捲動到頁面底部,觸發「載入更多」,更多內容插入,不算。
需要算分的是:廣告在使用者沒做任何操作的情況下突然出現;字型下載完成後替換掉 fallback 字型,導致段落重排;圖片沒有設定寬高,載入後撐開容器,把其他元素往下推。
這個區分很重要,因為如果你用了 accordion 或 tab 元件,使用者展開後的位移不是你需要修的問題,不用花時間處理這部分。
CLS 分數怎麼算
CLS 分數不是直接加總所有位移,而是取整個頁面生命週期中,分數最高的那個「Session Window」作為最終 CLS 分數。
Session Window 是一組連續位移事件的集合,定義是:相鄰兩次位移的間隔不超過 1 秒,整個窗口總時間不超過 5 秒。CLS 取所有窗口中分數最高的那個作為最終分數。這個機制防止長頁面因為頁面後段的少量位移被過度懲罰。
Impact Fraction 和 Distance Fraction
每次單一位移的分數計算公式:
Layout Shift Score = Impact Fraction × Distance Fraction
Impact Fraction(影響分數):發生位移前後,不穩定元素在 viewport 中佔據的面積聯集,除以 viewport 總面積。舉例:一個元素原本占 viewport 高度的 50%,向下移動後,原來位置加上新位置合計占 75%,影響分數就是 0.75。
Distance Fraction(距離分數):不穩定元素移動的最大距離,除以 viewport 的最長邊。同樣的例子:元素向下移動了 viewport 高度的 25%,距離分數就是 0.25。
這個位移的 layout shift score = 0.75 × 0.25 = 0.1875。這個單次分數已經超過 0.1 的門檻,如果這是最高 session window,CLS 就亮紅燈。
PageSpeed Insights、Lighthouse、GSC 的 CLS 為什麼不同
很多人遇到一個困惑:PSI 顯示 CLS 0.35 很差,但 GSC 顯示這個 URL 是「良好」,或反過來 GSC 說不良但 PSI 跑出來很好。這兩個數字衡量的不是同一件事。
| 工具 | 資料類型 | 資料來源 | 適合做什麼 |
|---|---|---|---|
| Lighthouse(PSI Lab Data) | Lab data(實驗室) | 模擬測試,固定條件 | 驗證修復是否生效、開發環境偵錯 |
| PageSpeed Insights Field Data | Field data(欄位) | CrUX 28 天真實用戶資料 | 了解真實使用者體驗 |
| GSC 核心網頁指標報告 | Field data(欄位) | CrUX URL 群組資料 | 監控整站 URL 群組狀況 |
| Chrome DevTools Performance | Lab data(實驗室) | 本地錄製,接近真實條件 | 定位具體位移元素與觸發時機 |
| Web Vitals Chrome Extension | Real-time field | 當前使用者當前環境 | 快速確認頁面當前 CLS 狀態 |
Lab data 和 field data 的差距很正常。一個常見的情況是:你用 PSI 跑固定模擬環境,廣告沒載入,CLS 看起來 0.05 很漂亮;但真實使用者有廣告,CrUX 記錄到 0.28。反過來也有:你的測試環境有慢速 CDN,PSI 跑出 0.3,但真實用戶有瀏覽器快取,field data 是 0.08。
排查問題時,先以 field data 判斷嚴重度,再以 lab data 定位問題和驗證修復。
CLS 問題要怎麼排查
排查 CLS 要先確認分數來源,再用工具找出位移元素,最後判斷觸發時機。不要一上來就改代碼,先搞清楚問題出在哪裡再動手修改。
第一步:GSC 核心網頁指標報告
打開 GSC → 體驗 → 核心網頁指標,選「行動裝置」或「電腦」,找標示「不良」的 URL 群組,點進去看哪些 URL 歸在這個群組。GSC 報告顯示的是 CrUX 欄位資料,反映真實使用者體驗,有流量門檻(URL 需要足夠流量才會出現)。
如果 GSC 沒有顯示具體 URL,可能是流量不足,或問題出在 origin summary 層級。這時改看 PSI 的「Origin summary」欄,以整個網域的平均值判斷。
第二步:PageSpeed Insights 診斷項目
拿 GSC 找到的問題 URL,丟進 PageSpeed Insights 跑分析。看「診斷」區域,找「避免重大的版面配置移位」這一項,Lighthouse 會列出造成位移的具體元素名稱。
這裡常看到的是:圖片元素(未設定 width/height)、廣告 iframe(載入後撐開版面)、字型替換造成的段落重排。Lighthouse 還會顯示每個元素對應的 layout shift score,讓你知道哪個貢獻最大。
第三步:Chrome DevTools Layout Shift 錄製
在 Chrome 打開要排查的頁面 → F12 開 DevTools → 切換到 Performance 面板 → 勾選「Web Vitals」 → 點擊錄製 → 重新載入頁面 → 停止錄製。
在時間軸上找紅色的 Layout Shift 標記,點進去會顯示哪些元素在那個時間點發生了位移,以及當時的 impact fraction 和 distance fraction。這能讓你精確知道是哪個 DOM 元素、在頁面載入第幾秒造成了問題。
也可以進 Rendering 設定,勾選「Layout Shift Regions」,讓瀏覽器用藍色框框即時標示每次位移的區域,不用錄製直接肉眼看。
最常造成 CLS 的五種原因
以下整理五種最常見的 CLS 原因,每一種都有固定的症狀模式和診斷路徑,對症下藥前先確認症狀,避免盲目猜測浪費排查時間。
圖片和影片沒有預設尺寸
最常見的原因。瀏覽器在圖片下載前不知道圖片多大,所以先不留空間,等圖片下載完再撐開,導致其他元素被推走。症狀是頁面載入過程中文字或按鈕突然往下跳。
修法:在 <img> 標籤加上 width 和 height 屬性,讓瀏覽器預先計算比例。
<!-- 錯誤:沒有尺寸,瀏覽器無法預留空間 -->
<img src="product.jpg" alt="商品圖">
<!-- 正確:指定寬高,瀏覽器知道要保留多少空間 -->
<img src="product.jpg" width="800" height="600" alt="商品圖">
搭配 CSS 讓圖片自適應容器寬度,同時保持比例:
img {
width: 100%;
height: auto;
}
現代 CSS 也可以直接用 aspect-ratio 屬性預留比例空間,不需要寫死數字:
.product-image {
aspect-ratio: 4 / 3;
width: 100%;
object-fit: cover;
}
廣告和嵌入內容的動態載入
廣告(AdSense、GPT、自定義廣告)通常在頁面載入後才非同步插入,如果版位沒有預留固定高度,廣告出現時就會把其他內容往下推。iframe 嵌入(YouTube 影片、地圖、第三方表單)也有同樣問題。
修法:為廣告版位預留固定的 min-height,讓廣告還沒載入時先占位,避免突然插入:
.ad-slot {
min-height: 250px; /* 根據你最常投放的廣告尺寸設定 */
width: 300px;
}
影片 iframe 加上 padding-bottom 技巧保持 16:9 比例:
.video-wrapper {
position: relative;
padding-bottom: 56.25%; /* 16:9 */
height: 0;
}
.video-wrapper iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
Web Font 造成的跳動
字型下載時,瀏覽器可能先用 fallback 字型顯示文字,等自定義字型下載完再換,這次替換如果字型度量不同,就會造成文字重排和位移。這叫 FOUT(Flash of Unstyled Text)。
處理方式有幾種,根據你的情況選:
- font-display: optional:如果字型在快取中,用它;否則直接用 fallback,不替換。幾乎消除 FOUT,但第一次訪問用戶看不到自定義字型。
- font-display: swap:先用 fallback,字型下載好後替換。有 FOUT,但字型一定會顯示。搭配
size-adjust讓 fallback 字型和目標字型的度量更接近,降低跳動幅度。 - preload 字型:在
<head>加<link rel="preload" as="font">,讓瀏覽器盡早開始下載字型,縮短 FOUT 時間窗。
/* font-display: optional:最穩定,但第一次訪問看不到自定義字型 */
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont.woff2') format('woff2');
font-display: optional;
}
動態插入的 DOM 元素
Cookie 同意橫幅在頁面載入後才從頂部彈出,把整個頁面往下推;推薦商品區塊在 API 回傳後才插入,把本來的內容推走;通知條(newsletter bar)出現在現有內容上方,這些都是常見的動態插入位移。
修法:不要插在既有內容的上方,改用 sticky 定位或在頁面底部顯示,或在 HTML 中預留固定高度的占位區塊,讓動態內容填入占位空間而不是推擠其他內容:
<!-- 錯誤:動態插入通知列到 body 頂部 -->
document.body.insertBefore(banner, document.body.firstChild);
<!-- 正確:預留固定占位容器 -->
<div id="banner-reserve" style="min-height: 50px;"></div>
動畫用 top/left 而非 transform
如果動畫用 top、left、margin、padding 這些屬性做位移,瀏覽器需要重新計算版面(Layout)並重繪,會觸發 layout shift。改用 transform: translate() 做位移,瀏覽器只在 composite 層處理,不影響版面計算,也不觸發 CLS 計分。
/* 避免:觸發 layout shift */
.element {
transition: top 0.3s ease;
}
.element.active {
top: 20px;
}
/* 建議:用 transform,不觸發 layout */
.element {
transition: transform 0.3s ease;
}
.element.active {
transform: translateY(20px);
}
修 CLS 要先改哪裡
上面五種原因你可能同時遇到,但資源有限,不可能一次全部修完。依照影響面積、重複出現率與修復難度,這是一個合理的優先順序:
- 首屏元素(Above-the-fold):頁面第一屏可見的圖片、廣告、Hero 區塊,對 CLS 影響最大,因為使用者在這裡最容易遇到位移,視窗越靠近頂部的位移影響面積越大。
- 全站共用模板元素:Header、導航、Banner 這類每頁都有的元素,修一次全站受益。這裡的位移通常影響所有 URL 的 CLS 分數。
- 高流量頁面的具體元素:先用 Lighthouse 診斷高流量的落地頁,修主要 layout shift 元素。
- 第三方腳本和廣告:難度較高,因為涉及外部控制。先給廣告版位設定 min-height,再研究更細緻的預留策略。
- 低流量頁面的細節位移:最後才處理,優先排序影響少、修復成本高的部分。
修完後分數怎麼還沒變
這是最常見的困惑。你把圖片加了 width/height,在 Lighthouse 跑一次 CLS 已經從 0.35 降到 0.02,但隔天看 GSC,還是顯示「不良」。
PSI 的 lab data(Lighthouse 模擬測試)是即時更新的,你修完馬上跑就能看到改善。但 GSC 和 PSI 的 field data 來自 CrUX(Chrome User Experience Report),是過去 28 天的真實使用者資料滾動平均。你今天修好了,舊的差分數還在資料池裡,要等 28 天讓新的好分數把舊數據稀釋掉,GSC 才會完整反映改善。
驗證節奏建議:
- 修完當天:用 Lighthouse / PSI lab data 確認技術上已修好。
- 修完 3-5 天:看 PSI 的 field data 欄位是否開始往好的方向移動。
- 修完 28 天:GSC 報告才會完整反映新狀態,如果 URL 群組從「不良」移到「良好」,代表真實使用者體驗已改善。
這個過程中不要反覆改代碼「試試看」,那只會讓資料更混亂,更難判斷哪次修改有效。
CLS 優化快速對照表
以下整理五種最常見的 CLS 原因、對應症狀、診斷工具與修復方向,可以對照排查清單,確認每個原因是否在你的頁面上出現過。
| 原因 | 症狀 | 診斷方式 | 修復方向 |
|---|---|---|---|
| 圖片未設定尺寸 | 圖片載入後文字往下跳 | PSI 診斷項目列出 img 元素 | 加 width + height 屬性,或 CSS aspect-ratio |
| 廣告動態載入 | 廣告出現後內容往下移 | DevTools 時間軸有廣告 iframe 插入事件 | 廣告版位加 min-height 占位 |
| Web Font FOUT | 字型替換後段落重排 | PSI 診斷項目列出字型相關元素 | font-display: optional 或 swap + size-adjust |
| 動態插入 DOM | 通知列或推薦區塊插入後整頁往下 | DevTools 找到 DOM 插入事件後的 layout shift | 改用固定占位容器或 sticky 定位 |
| 動畫用 top/left | 滑入動畫觸發版面重排 | Performance panel 動畫時點有 layout shift | 改用 transform: translate() |
相關資源與延伸閱讀
如果這篇文章解決了你的 CLS 問題,你可能也會遇到 Core Web Vitals 另外兩個指標的問題。LCP 最大內容繪製影響載入速度感知,常見原因是圖片沒有 preload 或 TTFB 過慢;INP 互動回應影響按鈕點擊的反應速度,通常與 JavaScript 執行時間有關。
在 GSC 查看完整的 Core Web Vitals 報告,以及如何用 GSC 監控各頁面群組的 CWV 狀態,參考 GSC 核心網頁指標報告使用教學。
官方資源:
- CLS 官方說明文件(web.dev,定義與計算)
- CLS 指標演進說明(web.dev,Session Window 解釋)
- 最小化廣告版面位移(Google 開發者,廣告相關修法)
如果你排查後發現問題出在網站模板、第三方 CMS 主題或廣告管理系統,難以自己修改,可以考慮做一次完整的技術 SEO 稽核,找出整站所有影響 Core Web Vitals 的問題來源。