# Cumulative Layout Shift (CLS) ![image](https://hackmd.io/_uploads/S1LgaH0SJl.png) - 非預期的 Layout Shifts 會以多種形式干擾使者體驗,可能導致找不到剛剛讀到哪、點錯按鈕等等,有時候甚至會造成嚴重的結果如 [此影片](https://web.dev/static/articles/cls/video/web-dev-assets/layout-instability-api/layout-instability2.webm)中使用者不小心送出原本想要取消的訂單 - 非預期的畫面改變,通常發生於資源以非同步的方式載入、DOM 元素以動態的方式被夾到畫面上;導致 Layout Shift 的可能是圖片或影片的尺寸不明、字型顯示的大小大於或小於初始的 fallback 或是會動態調整大小的第三方廣告或小工具 - 當開發環境和實際使用者體驗上的差異可能會造成更多問題 - 個人化或第三方內容在開發和實際運作時,通常會呈現不同的行為 - 測試圖片通常已存在開發人員的瀏覽器快取中,但對使用者來說,載入時間會更長 - 在本機執行的 API 呼叫通常速度非常快,因此在開發階段不易察覺的延遲,可能會在實際工作環境中變得相當明顯 - 透過累積版面配置偏移 (CLS) 指標可評估實際使用者發生這類問題的頻率,協助找出問題 ## 什麼是 CLS - 是用來衡量頁面整體穩定性的一項指標,具體指:『整個頁面生命周期內,所有 [非預期](https://web.dev/articles/cls#expected_vs_unexpected_layout_shifts) 布局偏移(layout shift)中,累積分數最高的「session window」的分數』 - 什麼是 Burst(或 [Session Window](https://web.dev/blog/evolving-cls#why_a_session_window))?([參考影片](https://web.dev/static/articles/cls/video/web-dev-assets/better-layout-shift-metric/session-window.webm)) - 指在一段時間內快速連續發生的一組布局偏移 - 定義:每次偏移之間的間隔小於 1 秒、整個窗口的總持續時間不超過 5 秒 - session 分數:每個 session 內的所有偏移分數累積 - CLS 計算方式 - 將整個頁面的生命周期分成多個 Session Window - 計算每個窗口內的 累積分數 - 取 累積分數最高的窗口 作為 CLS 分數 :::info 過去 CLS 會評估網頁整個生命週期中,所有 Session Window layout shift 分數的總和,導致頁面越長分數越高(即使後期的偏移對用戶影響很小) ::: ### 什麼是良好的 CLS 分數? - 為了提供良好的使用者體驗,網站應力求 CLS 分數為 0.1 以下 - 為確保多數使用者都能享有此等級的體驗,建議以第 75 個百分位數做為門檻,評估網頁載入情形,並區分行動裝置和電腦 ![image](https://hackmd.io/_uploads/S1LgaH0SJl.png) ## Layout Shift 詳情 - 由 [Layout Instability API](https://github.com/WICG/layout-instability) 定義,於可視區域內元素在兩個影格之間變更起始位置時,會報 `layout-shift`,而這些元素會被視為『不穩定元素』 - 如果是現有元素改變大小或DOM 中新增元素,只要沒有導致其他可見元素變更起始位置,都不會被視為 Layout Shift ### Layout Shift 分數計算 - 為了計算分數,瀏覽器會查看可視區域大小,和『不穩定元素』在兩個渲染影格之間的移動狀況 - 透過 `impact fraction`(影響因子)和 `distance fraction`(距離因子) 相成取得分數,兩個因子會在後面介紹 > layout shift score = impact fraction * distance fraction :::info 過去只會考量影響因子,但為避免大型元素只有移動少許距離產生的過度懲罰,引入了距離分數 ::: ### 影響因子(Impact Fraction) - 測量『不穩定元素』對兩個影格之間對 viewport 的影響程度 - 計算方式為將該影格和前一個養格中所有『不穩定元素』的可視區域相加,再除以可視範圍的總面積 ![image](https://hackmd.io/_uploads/H1nLMI0B1g.png) - 如上圖中一個元素佔用一個影格中一半的 viewport,於下一個影格中向下移動了 viewport 高度的 25% - 紅色虛線範圍表示兩個影格中原素可見區域的聯集,佔了總 viewport 的 75%,因此影響分數(Impact Fraction)為 0.75 ### 距離因子(Distance Fraction) - 測量『不穩定元素』相對於 viewport 移動的距離 - 計算方式為任何『不穩定元素』於影格之間移動的最大水平或垂直距離,除以 viewport 的最大尺算(寬度或高度,以較大者為準) ![image](https://hackmd.io/_uploads/HyAtBUCS1g.png) - 如上圖中 viewport 的最大尺寸為高度,而『不穩定元素』移動了 viewport 中的 25%,因此距離分數(Distance Fraction)為 0.25 > 因此基於範例來說,影響分數為 0.75,距離分數為 0.25, > Layout Shift 分數為 0.75 * 0.25 = 0.1875 > 這裡提供 [第二個範例](https://web.dev/articles/cls?hl=zh-tw#examples) ## 『預期』vs『非預期』的 Layout Shift > 並非所有的 Layout Shift 都是不好的,只有在使用者未預期的情況下才會造成不量影想 ### 使用者啟動的 Layout Shift - 只要在使用者互動 (例如點選連結、按下按鈕或在搜尋框中輸入內容) 後,Layout 變動情形不明顯,就不會違反規範 - e.g. 使用者互動觸發的網路請求可能需要一段時間才能完成,建議盡快建立一些空間並顯示載入畫面,以免在請求完成時造成不良的 Layout Shift;如果使用者無法知道系統正在載入,也無法知道資源何時載入完成,可能會在等待期間去點其他東西 - 在使用者輸入內容後的 500ms 內發生的 layout shift 會被視為 `hadRecentInput` (只針對 click、keypress 等事件,可以參考 [Layout Instability Spec](https://github.com/WICG/layout-instability#recent-input-exclusion) 了解更多)且被排除計算 ### 動畫和轉場效果 - 這個部分做得好可以讓網頁更新內容時不讓使用者感到意外 - 當內容在頁面上突然且意外的移動則會造成使用者的體驗不佳,但如果內容能自然地從一個位置移動到另一個位置,通常能幫助使用者進一步了解發生的情況,並在狀態變更之間引導他們 - 開發時應考量瀏覽器的 [`prefers-reduced-motion`](https://web.dev/articles/prefers-reduced-motion) 設置,主要為了照顧對動畫敏感的使用者 - 可以使用 CSS 的 `@media (prefers-reduced-motion:reduce)` 來檢測並調整相關動畫 - CSS `transform` 可以為元素設置動畫而不觸發 layout shift - 使用 `transform: scale()` 取代 `height` 或 `width` 屬性 - 如果要移動元素,盡量用 `transform: translate()` 取代 `top`、`right`、`bottom`、`left` 屬性 ## CLS Optimization - 造成 CLS 不佳常見的原因包括 - 沒有尺寸的圖片 - 廣告、嵌入內容和 iframe 沒有尺寸 - 動態插入的內容,例如廣告、嵌入內容和 iframe,且不含尺寸 - 網頁字型變化 - 可以參考 [這隻影片](https://www.youtube.com/watch?v=AQqFZ5t8uNc),解析一名為 Chloe 的網站如何改善他們的 CLS ### 透過 Lab Tools (實驗室工具) vs Field Data(實地測試) 測量 CLS - 開發人員經常認為 [Chrome UX 報告 (CrUX)](https://developer.chrome.com/docs/crux?hl=zh-tw) 所測量的 CLS 不正確,因為與他們使用 Chrome 開發人員工具或其他實驗室工具所測量的 CLS 不符 - CrUX 是 Web Vitals 計畫的官方 dataset,因此 CLS 會在整個網頁生命週期中進行評估,而非像實驗室工具通常只會評估初始網頁載入期間 - Lighthouse 等網頁效能實驗室工具通常會簡單載入網頁,以便評估某些網頁效能指標並提供相關指引,因此可能不會顯示網頁的完整 CLS - 可以透過 puppeteer 這種工具模仿使用者行為搭配 lighthouse 來量測網頁載入外的項目 - 在頁面載入期間 Layout Shifts 非常常見,系統會擷取所有必要資源以便初始渲染網頁,但 Layout Shifts 也可能發生在初始載入後 - 許多後載入後的位移可能會因使用者互動而發生,會從 CLS 分數中排除,因為這些移位是預期的移位,只要發生在互動後的 500 毫秒內即可 - 其他不是使用者互動導致的『非預期』位移就會被算到 CLS 分數中 - e.g 捲動畫面查看更多時,Lazy Load 進來的東西載入導致位移 - 另一種狀況是在 SPA 的轉場互動中,可能會超過 500ms 的寬限期 - [PageSpeed Insights](https://pagespeed.web.dev/?hl=zh-tw) 會在「瞭解實際使用者體驗」部分顯示網址的使用者感知 CLS,並在「診斷效能問題」部分顯示實驗室負載 CLS。這些值之間的差異可能是後載入 CLS 的結果 ![image](https://hackmd.io/_uploads/BJ6VLkyI1g.png) - 這裡發現 CrUX 測量到的 CLS 比 Lighthouse 高出許多 - 注意使用 PageSpeed Insights 時,它會顯示兩種層級的資料(右上角的選項) - URL 層級資料 (URL-level data):針對特定頁面 URL 的性能數據,如果該頁面有專屬的數據,工具會優先顯示這個層級的資料 - 來源層級資料 (Origin-level data):如果特定頁面的數據不可用,工具會回退到使用該網站的整體數據(以域名為基準,如 https://example.com) ### 找出載入時的 CLS 問題 - 如果 PageSpeed Insights 的 CrUX 和 lighthouse 的 CLS 分數大致一致,通常代表 lighthouse 有偵測到載入時的 CLS 問題 - 在這種情況下,Lighthouse 會協助進行兩項診斷來提供更多資訊 - 說明圖片因缺少寬度和高度而導致 CLS - 列出所有因網頁載入而位移的元素,以及這些元素對 CLS 的影響 - 可以在右上角篩選出 CLS 相關的診斷 ![image](https://hackmd.io/_uploads/Skw3u11Ike.png) - Lighthouse 會找出已移動的元素,但這些元素通常是受影響的元素,而非造成 CLS 的元素,但通常只要有移位的元素就能找出並解決根本原因 - e.g. DOM 中插入了新元素,這項診斷會顯示該元素下方的元素,但根本原因是新增了上方的新元素 - 透過 DevTools 中的『[Performance](https://developer.chrome.com/docs/devtools/performance?hl=zh-tw)』面板中的 『Experience』區塊會標出 Layout Shift - 紀錄 Layout Shift 的『Summary』view 除了包含 CLS 分數外,還能顯示受影響區域的矩形標示(疊加層),特別有助於分析頁面載入時的 CLS 問題,因為可以透過重新載入 Performance Profile 輕鬆地重現問題 ![image](https://hackmd.io/_uploads/HyLW21kIJg.png) ### 找出載入後的 CLS 問題 - 如果 CrUX 和 lighthouse 的 CLS 分數不一致,通常代表有載入『後』的 CLS 問題 - 在沒有實地資料的狀況下要追蹤會變得很困難,而實地資料可以透過以下方式盡量取得 - [Web Vitals Chrome extension](https://chrome.google.com/webstore/detail/web-vitals/ahfhijdlegdabablpippeagghigmibma) 可以在和網頁互動的過程中監控 CLS 並顯示在 HUD 或 console,取得更多關於產生 Layout Shift 的元素資訊,參考 [這篇](https://web.dev/articles/debug-cwvs-with-web-vitals-extension) 了解使用方式 - 除了用 Extension 外,也可以透過 [Performance Observer](https://web.dev/articles/cls#measure_layout_shifts_in_javascript) 紀錄 Layout Shifts - 建立監控後就可以開始嘗試重現造成載入後 CLS 問題的情境 - 通常發生在使用者捲動頁面時沒有保留空間給 lazy load 進來的內容 - 另一種常見的是使用者 hover 的時候內容產生位移 - 找出主要問題來源後,可以透過 e2e 工具搭配 lighthouse 來模擬使用者互動並產出報告,可以參考 [這篇](https://web.dev/articles/lighthouse-user-flows#timespans) 詳細內容 ## 造成 CLS 的常見原因 ### 未指定尺寸的圖片 - 一定要在圖片和影片元素上加上 `width` 和 `height` 屬性,也可以透過 CSS 中的 `aspect-ratio` 或是類似作法來預留所需要的空間,這樣做能確保瀏覽器載圖片載入期間能正確分配頁面上的空間 - 參考兩個影片對比 [沒有預留](https://web.dev/static/articles/optimize-cls/video/tcFciHGuF3MxnTr1y5ue01OGLBn2/10TEOBGBqZm1SEXE7KiC.webm?hl=zh-tw) vs [有預留](https://web.dev/static/articles/optimize-cls/video/tcFciHGuF3MxnTr1y5ue01OGLBn2/38UiHViz44OWqlKFe1VC.webm?hl=zh-tw) - 過去會直接在這些元素上面設置的 `width` 和 `height` 屬性,無論實際尺寸是否符合,圖片都會延展至這個空間 ```html <img src="puppy.jpg" width="640" height="360" alt="Puppy with balloons"> ``` - 在 RWD 出現後,開發人員通常會用 CSS 調整大小,取代了元素上的 `width` 和 `height` 屬性 ```css img { width: 100%; /* or max-width: 100%; */ height: auto; } ``` - 然而因為沒有設置實際大小,瀏覽器要等到下載完圖片、判斷尺寸後才能幫圖片分配空間,可能導致為騰出空間而擠壓到其他元素產生位移 - 這時就可以透過 `aspect-ratio` 讓瀏覽器根據比例由寬度找出高度(16:9 = 寬:高),或是反過來,進而知道該預留多少空間 - Best Practice - 由於現今的瀏覽器會會使用圖片本身寬高作為預設的 aspect ratio,因此直接在圖片上設置 `width` 和 `height` 屬性可以讓瀏覽器直接用這些值設置比例,接著在加上如上的 CSS 來避免版面偏移 ```html <!-- set a 640:360 i.e a 16:9 aspect ratio --> <img src="puppy.jpg" width="640" height="360" alt="Puppy with balloons"> ``` 圖片載入前,會根據我們所設置的 `width` 和 `height` 屬性計算比例,當我們透過 CSS 將圖片寬度設為某個值(e.g. width: 100%),系統就會用這個比例來計算對應的高度 - CSS 中的 aspect-ratio 值是在處理 HTML 時由瀏覽器計算,而不是使用預設的 User-Agent StyleSheet(參考 這篇 理解原因)。由於這是瀏覽器內部的行為,不同瀏覽器對於如何計算和呈現 aspect-ratio 的顯示會有些許差異(此處特指瀏覽器如何計算和呈現自動生成的 aspect-ratio 值,而不是你在 CSS 中設置的屬性) - Chrome 和 Safari:在 DevTools 中,這些計算值會以如下形式呈現: ```css img[Attributes Style] { aspect-ratio: auto 640 / 360; } ``` - auto 表示這個比例是基於圖片的實際尺寸自動計算的。當圖片下載完成後,瀏覽器會用圖片的實際尺寸來覆蓋原本的寬高比,以確保佈局的正確性 - Firefox:不會在 DevTools 的 Styles 面板中顯示這個計算值,但它仍然會利用這些數值來進行版面配置 - 如果你在 CSS 中設置了 aspect-ratio,則瀏覽器將使用你明確設置的值來覆蓋這個自動計算的行為,進一步控制佈局和顯示的比例 - 當圖片被放在容器中,可以用 CSS 將圖片大小調整為容器寬度 ```css .container > img { height: auto; // default value width: 100%; } ``` - 在處理響應式圖片時,`srcset` 可以定義瀏覽器可選取的圖片大小,為了確保 `<img>` 標籤上能設置 `width` 和 `height`,這些圖片應該要有相同的 aspect ratio ```html <img width="1000" height="1000" src="puppy-1000.jpg" srcset="puppy-1000.jpg 1000w, puppy-2000.jpg 2000w, puppy-3000.jpg 3000w" alt="Puppy with balloons" /> ``` - 圖片本身的 aspect ratio 在不同的 [art direction](https://developer.mozilla.org/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images#Art_direction) 底下也有可能改變 > art direction: 針對不同情境下(如螢幕大小、裝置類型等)進行圖片的設計和裁剪,來呈現最佳視覺效果,而不僅僅是調整圖片的比例 - 例如想為狹窄的檢視區提供經過裁剪的圖片,並在電腦上顯示完整圖片 ```html <picture> <source media="(max-width: 799px)" srcset="puppy-480w-cropped.jpg" /> <source media="(min-width: 800px)" srcset="puppy-800w.jpg" /> <img src="puppy-800w.jpg" alt="Puppy with balloons" /> </picture> ``` - Chrome、Firefox 和 Safari 現在支援在特定 `<picture>` 元素中的 `<source>` 元素上設定 width 和 height ```html <picture> <source media="(max-width: 799px)" srcset="puppy-480w-cropped.jpg" width="480" height="400" /> <source media="(min-width: 800px)" srcset="puppy-800w.jpg" width="800" height="400" /> <img src="puppy-800w.jpg" alt="Puppy with balloons" width="800" height="400" /> </picture> ``` ### 廣告、嵌入內容和其他延遲載入的內容 - 廣告、嵌入內容、iframe 等動態插入的內容,也有可能導致內容被向下推移而提高 CLS - 動態大小的廣告、嵌入式小工具,在控制權於第三方或是無法事先知道內容大小的情況下,仍無法控制版面變化 - 為延遲載入的內容在初始版面配置中預留空間 - 新增 CSS `min-height` 或 `aspect ratio` 等類似方式來保留空間 ![image](https://hackmd.io/_uploads/rJSGRayUyg.png) - 可能需要透過 media-query 針對不同版型預留不同空間尺寸 - 如果沒有固定高度的內容,可能導致無法保留足夠空間來解決 Layout Shift - 針對小一點的廣告可以提供大一點的預留空間,或是根據過去的可能大小設置(缺點是會增加頁面上的空白空間) - 可以透過如 `min-height` 設置初始大小且彈性接受較大的尺寸,相較於大小為 0 的狀態,能某種程度減少 layout shift - 將延遲載入的內容放在可視區域的下方,能多少減少一些位移 - 避免在未經使用者互動情況下插入新內容 - 載入網頁時,有時會在檢視區頂端或底部看到彈出的 UI 導致位移,如果有這種 UI 的需要,記得也要預留空間 - 將新內容放入固定大小的容器中替換舊內容,或使用輪播(carousel)進行過渡,並在過渡完成後移除舊內容。記得在過渡期間禁用所有連結和控制項,以防止用戶在過渡過程中意外點擊 - 讓用戶主動觸發新內容的加載(例如透過 "加載更多" 或 "刷新" 按鈕),以避免用戶對頁面變化感到驚訝。建議在用戶交互前預先載入內容,確保用戶操作後能立即顯示 - 在螢幕外加載新內容,並向用戶顯示提示通知(例如 "向上滾動查看" 按鈕),讓用戶知道新內容已可用 ### 動畫 - 有些 CSS 屬性的變更,會導致瀏覽器需要跟著進行 re-layout、repaint 和 composite,可能進而導致 Layout Shift,應此應盡量避免使用這些屬性產生動畫 - e.g. `box-sizing`、`box-shadow`、`left`、`top` - 使用 `transform` 建立動畫可以避免觸發 re-layout,可以參考 [High-performance animations](https://web.dev/articles/animations-guide) 了解更多 ### 網頁字型 - 下載字型後渲染出 web fonts 通常有兩種形式 - Fallback 字型會被 web font 取代,產生 Flash of Unstyled Text (FOUT) - 系統會使用 Fallback 字型顯示「隱藏」文字,直到 web font 可用且文字顯示出來為止,這是 Flash of Invisible Text (FOIT) - 上面兩種都有可能導致 Layout Shift,因為就算文字看不見他還是會用 Fallback 字型進行版面配置,載入後文字周圍的元素還是有可能被影響 - 以下幾種方式可以盡可能減低位移 - `font-display: optional` 可以避免 re-layout,因為只有在初始化版面配置時有可用的 web font 才會使用 - 使用合適的 fallback 字型(長得類似) - 透過一些提供的 API 來盡可能降低 fallback font 和 web font 的大小差異,參考 [這篇文章](https://developer.chrome.com/blog/font-fallbacks) 了解 - [Font Loading API](https://web.dev/articles/optimize-webfont-loading?hl=zh-tw#the_font_loading_api) 可以縮短取得字型的時間 - 盡可能使用 `<link rel=preload>` 載入重要 web font,讓 First Paint 時就能使用預先載入的字型 - 更多可以參考 [Best practices for fonts](https://web.dev/articles/font-best-practices) ## 透過啟用 bfcache 減少 CLS 分數 - [bfcache(back/forward cache)](https://web.dev/articles/bfcache?hl=zh-tw)是一種將頁面短暫保存在瀏覽器記憶體中的技術,當用戶返回頁面時,頁面會以離開時的狀態快速恢復 - 恢復的頁面完全載入且不會因載入流程產生 Layout Shift,從而降低 CLS 分數 - bfcache 無法避免首次載入時的 Layout Shift,因此應盡量優化首次載入的內容,但可以避免用戶在返回內容頁、分類頁或搜尋結果頁時,重複看到 Layout Shift - 所有現代瀏覽器預設使用 bfcache,但某些網站因不符合條件而無法啟用 - 應測試頁面是否符合 bfcache 使用條件,識別並解決任何阻止 bfcache 的問題 - 參考 [bfcache 指南](https://web.dev/articles/bfcache?hl=zh-tw) 了解更多可能妨礙 bfcache 使用的問題 ## 參考來源 - [web.dev - Cumulative Layout Shift (CLS)](https://web.dev/articles/cls) - [web.dev - optimize-cls](https://web.dev/articles/optimize-cls)