如何修復累積佈局移位 (CLS) 問題
已發表: 2022-03-10Cumulative Layout Shift (CLS) 嘗試將頁面的那些不和諧的移動測量為新內容——無論是圖像、廣告還是其他任何東西——比頁面的其餘部分更晚發揮作用。 它根據頁面的意外移動量和頻率來計算分數。 這些內容的變化非常煩人,使您在已開始閱讀的文章中失去自己的位置,或者更糟糕的是,使您單擊了錯誤的按鈕!
在本文中,我將討論一些減少 CLS 的前端模式。 我不會過多談論如何測量 CLS,因為我已經在之前的文章中介紹過。 我也不會過多談論如何計算 CLS 的機制:Google 對此有一些很好的文檔,而 Jess Peck 的 The Near-Complete Guide to Cumulative Layout Shift 也是一個很棒的深入探討。 但是,我將提供一些了解一些技術所需的背景知識。
為什麼 CLS 與眾不同
在我看來,CLS 是 Core Web Vitals 中最有趣的,部分原因是我們以前從未真正衡量或優化過它。 因此,它通常需要新的技術和思維方式來嘗試對其進行優化。 它與其他兩個 Core Web Vitals 非常不同。
簡要回顧一下其他兩個核心 Web Vitals,Largest Contentful Paint (LCP) 就像它的名字所暗示的那樣,它更像是對以前衡量頁面加載速度的加載指標的一種扭曲。 是的,我們改變了定義頁面加載用戶體驗的方式,以查看最相關內容的加載速度,但它基本上是重用確保內容盡快加載的舊技術。 對於大多數網頁來說,如何優化你的 LCP 應該是一個比較容易理解的問題。
首次輸入延遲 (FID) 測量交互中的任何延遲,對於大多數站點來說似乎不是問題。 優化這通常是清理(或減少!)您的 JavaScript 的問題,並且通常是特定於站點的。 這並不是說用這兩個指標解決問題很容易,但它們是相當容易理解的問題。
CLS 不同的一個原因是它是通過頁面的生命週期來衡量的——這是名稱的“累積”部分! 其他兩個 Core Web Vitals 在加載後(對於 LCP)或第一次交互(對於 FID)後在頁面上找到主要組件後停止。 這意味著我們傳統的基於實驗室的工具(如 Lighthouse)通常不能完全反映 CLS,因為它們僅計算初始負載 CLS。 在現實生活中,用戶將向下滾動頁面,可能會導致更多內容下降,從而導致更多變化。
CLS 也是一個人為的數字,它是根據頁面的移動量和頻率計算得出的。 雖然 LCP 和 FID 以毫秒為單位測量,但CLS 是通過複雜計算輸出的無單位數。 我們希望頁面為 0.1 或以下以通過此 Core Web Vital。 任何高於 0.25 的值都被視為“差”。
由用戶交互引起的班次不計算在內。 這被定義為在一組特定用戶交互的500 毫秒內,但不包括指針事件和滾動。 假定單擊按鈕的用戶可能期望出現內容,例如通過展開折疊的部分。
CLS 是關於測量意想不到的變化。 如果頁面構建得最佳,滾動不應導致內容四處移動,並且類似地,將鼠標懸停在產品圖像上以獲得放大版本也不應導致其他內容跳躍。 但當然也有例外,這些網站需要考慮如何應對。
CLS 也隨著調整和錯誤修復不斷發展。 它剛剛宣布了一個更大的變化,應該為長期存在的頁面提供一些喘息的機會,比如單頁應用程序 (SPA) 和無限滾動頁面,許多人認為這些頁面在 CLS 中受到了不公平的懲罰。 不是像迄今為止所做的那樣在整個頁面時間上累積班次來計算 CLS 分數,而是根據特定時間框窗口內的最大班次集來計算分數。
這意味著如果您有 0.05、0.06 和 0.04 的三個 CLS 塊,那麼以前這將被記錄為 0.15(即超過 0.1 的“好”限制),而現在將被評分為 0.06。 它仍然是累積的,因為分數可能由該時間範圍內的單獨班次組成(即,如果 0.06 CLS 分數是由 0.02 的三個單獨班次引起的),但它不再在頁面的總生命週期內累積.
也就是說,如果您解決了 0.06 偏移的原因,那麼您的 CLS 將被報告為下一個最大的偏移 (0.05),因此它仍在查看頁面生命週期內的所有偏移——它只是選擇僅報告最大的一個作為 CLS 分數。
通過對有關 CLS 的一些方法的簡要介紹,讓我們繼續討論一些解決方案! 所有這些技術基本上都涉及在加載其他內容之前留出正確的空間量——無論是媒體還是 JavaScript 注入的內容,但是 Web 開發人員可以使用一些不同的選項來執行此操作。
在圖像和 iFrame 上設置寬度和高度
我之前已經寫過這方面的內容,但是減少 CLS 可以做的最簡單的事情之一就是確保在圖像上設置了width
和height
屬性。 沒有它們,圖像將導致後續內容在下載後轉移為它讓路:
這只是將圖像標記從以下位置更改的問題:
<img src="hero_image.jpg" alt="...">
到:
<img src="hero_image.jpg" alt="..." width="400" height="400">
您可以通過打開 DevTools 並將鼠標懸停在(或點擊)元素上找到圖像的尺寸。
我建議使用Intrinsic Size (這是圖像源的實際大小),然後當您使用 CSS 更改這些大小時,瀏覽器會將它們縮小到呈現的大小。
快速提示:如果像我一樣,你不記得它是寬度和高度還是高度和寬度,把它想像成 X 和 Y 坐標,所以像 X 一樣,寬度總是首先給出。
如果您有響應式圖像並使用 CSS 更改圖像尺寸(例如,將其限制為屏幕大小的 100% 的max-width
),那麼這些屬性可用於計算height
- 前提是您記得將其覆蓋為在你的 CSS 中auto
:
img { max-width: 100%; height: auto; }
所有現代瀏覽器現在都支持這一點,但直到最近才如我的文章中所述。 這也適用於<picture>
元素和srcset
圖像(在後備img
元素上設置width
和height
),雖然還不適用於不同縱橫比的圖像——它正在處理中,在此之前你仍然應該設置width
和height
因為任何值都將優於0
by 0
默認值!
這也適用於本機延遲加載的圖像(儘管 Safari 默認不支持本機延遲加載)。
新aspect-ratio
CSS 屬性
上面的width
和height
技術,用於計算響應式圖像的高度,可以使用新的 CSS aspect-ratio
屬性推廣到其他元素,現在基於 Chromium 的瀏覽器和 Firefox 支持,但也在 Safari 技術預覽中,所以希望這意味著它將很快進入穩定版本。
因此,您可以在嵌入式視頻上使用它,例如以 16:9 的比例:
video { max-width: 100%; height: auto; aspect-ratio: 16 / 9; }
<video controls width="1600" height="900" poster="..."> <source src="/media/video.webm" type="video/webm"> <source src="/media/video.mp4" type="video/mp4"> Sorry, your browser doesn't support embedded videos. </video>
有趣的是,如果沒有定義aspect-ratio
屬性,瀏覽器將忽略響應式視頻元素的高度並使用默認的縱橫比 2:1,因此需要上述內容來避免此處的佈局偏移。
將來,甚至應該可以使用寬高比根據元素屬性動態設置aspect-ratio
aspect-ratio: attr(width) / attr(height);
但遺憾的是,這還不被支持。
或者,您甚至可以在<div>
元素上使用aspect-ratio
來創建某種自定義控件,以使其具有響應性:
#my-square-custom-control { max-width: 100%; height: auto; width: 500px; aspect-ratio: 1; }
<div></div>
對於那些不支持aspect-ratio
的瀏覽器,您可以使用舊的 padding-bottom hack,但是,由於新aspect-ratio
的簡單性和廣泛的支持(尤其是從 Safari Technical Preview 轉移到常規 Safari 時),它是很難證明這種舊方法的合理性。
Chrome 是唯一將 CLS 反饋給 Google的瀏覽器,它支持aspect-ratio
,這意味著將解決您在 Core Web Vitals 方面的 CLS 問題。 我不喜歡將指標優先於用戶,但事實上其他 Chromium 和 Firefox 瀏覽器都有這個,而 Safari 有望很快實現,而且這是一個漸進式增強意味著我想說我們正處於我們的階段可以留下 padding-bottom hack 並編寫更清晰的代碼。
自由使用min-height
對於那些不需要響應大小但需要固定高度的元素,請考慮使用min-height
。 例如,這可能是一個固定高度的 header ,我們可以像往常一樣使用媒體查詢為不同的斷點設置不同的標題:
header { min-height: 50px; } @media (min-width: 600px) { header { min-height: 200px; } }
<header> ... </header>
當然,這同樣適用於水平放置元素的min-width
,但通常是導致 CLS 問題的高度。
注入內容和高級 CSS 選擇器的更高級技術是在尚未插入預期內容時定位。 例如,如果您有以下內容:
<div class="container"> <div class="main-content">...</div> </div>
並通過 JavaScript 插入一個額外的div
:
<div class="container"> <div class="additional-content">.../div> <div class="main-content">...</div> </div>
然後,您可以使用以下代碼段在最初呈現main-content
div 時為其他內容留出空間。
.main-content:first-child { margin-top: 20px; }
這段代碼實際上會創建一個到main-content
元素的移位,因為邊距算作該元素的一部分,因此當它被刪除時它會出現移位(即使它實際上並沒有在屏幕上移動)。 但是,至少它下面的內容不會被移動,所以應該減少 CLS。
或者,您也可以使用::before
偽元素添加空格以避免main-content
元素上的移位:
.main-content:first-child::before { content: ''; min-height: 20px; display: block; }
但老實說,更好的解決方案是在 HTML 中使用div
並在其上使用min-height
。
檢查後備元素
我喜歡使用漸進增強來提供一個基本的網站,即使可能沒有 JavaScript。 不幸的是,最近在我維護的一個站點上,當備用非 JavaScript 版本與 JavaScript 啟動時不同時,我發現了這一點。
問題是由於標題中的“目錄”菜單按鈕造成的。 在 JavaScript 啟動之前,這是一個簡單的鏈接,其樣式看起來像將您帶到目錄頁面的按鈕。 一旦 JavaScript 啟動,它就會變成一個動態菜單,讓您可以直接導航到您想從該頁面轉到的任何頁面。
我使用了語義元素,因此使用了一個錨元素( <a href="#table-of-contents">
)作為後備鏈接,但用<button>
代替了它作為 JavaScript 驅動的動態菜單。 這些樣式看起來相同,但後備鏈接比按鈕小幾個像素!
它是如此之小,而且 JavaScript 通常啟動得如此之快,以至於我沒有註意到它已關閉。 然而,Chrome 在計算 CLS 時注意到了這一點,並且由於它位於標題中,因此它將整個頁面向下移動了幾個像素。 所以這對 CLS 分數產生了相當大的影響——足以讓我們所有的頁面都進入“需要改進”類別。
這是我的一個錯誤,修復只是讓兩個元素同步(也可以通過在標題上設置min-height
來修復,如上所述),但這讓我有點困惑。 我確定我不是唯一犯此錯誤的人,因此請注意頁面在沒有 JavaScript 的情況下如何呈現。 不要認為您的用戶禁用了 JavaScript? 您的所有用戶在下載您的 JS 時都是非 JS。
網頁字體導致佈局變化
Web 字體是 CLS 的另一個常見原因,因為瀏覽器最初會根據備用字體計算所需的空間,然後在下載 Web 字體時重新計算它。 通常,CLS 很小,提供類似大小的後備字體,因此它們通常不會導致足以使 Core Web Vitals 失敗的問題,但它們可能會令用戶感到不安。
不幸的是,即使預加載 webfonts 也無濟於事,因為雖然這減少了使用後備字體的時間(因此有利於加載性能 - LCP),但仍需要時間來獲取它們,因此仍將使用後備字體在大多數情況下由瀏覽器進行,因此不會避免 CLS。 也就是說,如果您知道下一頁需要網絡字體(假設您在登錄頁面上並且知道下一頁使用特殊字體),那麼您可以預取它們。
為了完全避免字體引起的佈局變化,我們當然可以完全不使用網絡字體——包括使用系統字體,或者使用font-display: optional
不使用它們。 但老實說,這些都不是很令人滿意。
另一種選擇是確保這些部分的大小適當(例如使用min-height
),因此雖然其中的文本可能會發生一些變化,但即使發生這種情況,它下面的內容也不會被下推。 例如,在<h1>
元素上設置min-height
可以防止在加載稍高的字體時整篇文章向下移動——前提是不同的字體不會導致不同的行數。 這將減少變化的影響,但是,對於許多用例(例如通用段落),很難概括最小高度。
解決這個問題我最興奮的是新的 CSS 字體描述符,它允許您更輕鬆地調整 CSS 中的後備字體:
@font-face { font-family: 'Lato'; src: url('/static/fonts/Lato.woff2') format('woff2'); font-weight: 400; } @font-face { font-family: "Lato-fallback"; size-adjust: 97.38%; ascent-override: 99%; src: local("Arial"); } h1 { font-family: Lato, Lato-fallback, sans-serif; }
在此之前,使用 JavaScript 中的 Font Loading API 調整所需的備用字體更複雜,但這個選項很快就會出現,最終可能會給我們一個更簡單的解決方案,更有可能獲得牽引力。 請參閱我之前關於此主題的文章,了解有關此即將推出的創新的更多詳細信息以及更多相關資源。
客戶端渲染頁面的初始模板
許多客戶端渲染頁面或單頁應用程序僅使用 HTML 和 CSS 渲染初始基本頁面,然後在 JavaScript 下載並執行後“水合”模板。
這些初始模板很容易與 JavaScript 版本不同步,因為新組件和功能在 JavaScript 中添加到應用程序中,但未添加到首先呈現的初始 HTML 模板中。 當這些組件被 JavaScript 注入時,這會導致 CLS。
因此,請檢查所有初始模板以確保它們仍然是良好的初始佔位符。 如果初始模板由空<div>
組成,則使用上述技術確保它們的大小適當,以避免任何變化。
此外,與應用程序一起注入的初始div
應具有min-height
,以避免在插入初始模板之前最初以 0 高度呈現它。
<div></div>
例如,只要min-height
大於大多數 viewports ,這應該避免網站頁腳的任何 CLS。 CLS 僅在它位於視口中時才會測量,因此會影響用戶。 默認情況下,空div
的高度為 0px,因此給它一個更接近應用加載時實際高度的min-height
。
確保用戶交互在 500 毫秒內完成
導致內容轉移的用戶交互不包括在 CLS 分數中。 這些限制在交互後的 500 毫秒內。 因此,如果您單擊一個按鈕,並進行一些耗時超過 500 毫秒的複雜處理,然後渲染一些新內容,那麼您的 CLS 分數將會受到影響。
您可以通過使用“性能”選項卡記錄頁面,然後找到下一個屏幕截圖所示的班次,來查看是否在 Chrome DevTools 中排除了班次。 打開 DevTools 轉到非常嚇人的(但一旦掌握它就非常有用!)性能選項卡,然後單擊左上角的記錄按鈕(在下圖中圈出)並與您的頁面交互,並停止記錄一次完全的。
您將看到頁面的幻燈片,其中我加載了另一篇 Smashing Magazine 文章的一些評論,因此在我圈出的部分中,您幾乎可以看到評論加載和紅色頁腳被移出屏幕。 在Performance選項卡的下方,在Experience行下方,Chrome 將為每個班次放置一個淡紅色的框,當您單擊該框時,您將在下面的Summary選項卡中獲得更多詳細信息。
在這裡您可以看到我們得到了0.3359 的巨大分數——遠遠超過了我們的目標是低於 0.1 的閾值,但累積分數沒有包括這個,因為最近的輸入設置為使用。
確保交互僅在 First Input Delay 嘗試測量的 500 ms 邊界內移動內容,但在某些情況下,用戶可能會看到輸入產生了影響(例如顯示了加載微調器),因此 FID 很好,但內容可能直到 500 毫秒限制之後才被添加到頁面,所以 CLS 不好。
理想情況下,整個交互將在 500 毫秒內完成,但是您可以在處理過程中使用上述技術做一些事情來留出必要的空間,這樣如果它確實需要超過神奇的 500 毫秒,那麼您已經已經處理了班次,因此不會因此受到處罰。 這在從網絡獲取可能是可變的且超出您控制範圍的內容時特別有用。
其他需要注意的項目是時間超過 500 毫秒的動畫,因此會影響 CLS。 雖然這看起來有點限制,但 CLS 的目的不是限制“樂趣”,而是設定對用戶體驗的合理期望,我認為期望這些時間花費 500 毫秒或以下並不現實。 但是,如果您不同意,或者有他們可能沒有考慮過的用例,那麼 Chrome 團隊願意就此提供反饋。
同步 JavaScript
我要討論的最後一種技術有點爭議,因為它違背了眾所周知的 Web 性能建議,但在某些情況下它可能是唯一的方法。 基本上,如果您知道內容會導致變化,那麼避免變化的一種解決方案是在它穩定下來之前不渲染它!
下面的 HTML 最初會隱藏div
,然後加載一些阻止渲染的 JavaScript 來填充div
,然後取消隱藏它。 由於 JavaScript 正在渲染阻止,因此不會渲染任何低於此的內容(包括取消隱藏它的第二個style
塊),因此不會發生任何變化。
<style> .cls-inducing-div { display: none; } </style> <div class="cls-inducing-div"></div> <script> ... </script> <style> .cls-inducing-div { display: block; } </style>
使用這種技術在 HTML 中內聯 CSS很重要,因此它是按順序應用的。 另一種方法是使用 JavaScript 本身取消隱藏內容,但我喜歡上述技術的是,即使 JavaScript 失敗或被瀏覽器關閉,它仍然會取消隱藏內容。
這種技術甚至可以應用於外部 JavaScript,但是這會比內聯script
造成更多的延遲,因為外部 JavaScript 被請求和下載。 這種延遲可以通過預加載 JavaScript 資源來最小化,這樣一旦解析器到達那段代碼就可以更快地使用它:
<head> ... <link rel="preload" href="cls-inducing-javascript.js" as="script"> ... </head> <body> ... <style> .cls-inducing-div { display: none; } </style> <div class="cls-inducing-div"></div> <script src="cls-inducing-javascript.js"></script> <style> .cls-inducing-div { display: block; } </style> ... </body>
現在,正如我所說,這肯定會讓一些 web 性能的人感到畏縮,因為建議在 JavaScript 上使用async, defer
或更新的type="module"
(默認情況下是defer
-ed)以避免阻塞render ,而我們在這裡做相反的事情! 但是,如果內容無法預先確定並且會引起不和諧的變化,那麼提前渲染它就沒有什麼意義了。
我將這種技術用於加載在頁面頂部並將內容向下移動的cookie 橫幅:
這需要讀取 cookie 以查看是否顯示 cookie 橫幅,雖然這可以在服務器端完成,但這是一個靜態站點,無法動態更改返回的 HTML。
Cookie 橫幅可以以不同的方式實現以避免 CLS。 例如,將它們放在頁面底部,或者將它們覆蓋在內容之上,而不是將內容向下移動。 我們更喜歡將內容保留在頁面頂部,因此必須使用這種技術來避免移位。 由於各種原因,網站所有者可能更喜歡在頁面頂部顯示其他各種警報和橫幅。
我還在另一個頁面上使用了這種技術,其中JavaScript 將內容移動到“主”和“輔助”列中(由於我不會深入討論的原因,不可能在 HTML 服務器端正確地構造它)。 再次隱藏內容,直到 JavaScript 重新排列內容,然後才顯示它,避免了導致這些頁面的 CLS 分數下降的 CLS 問題。 即使 JavaScript 由於某種原因沒有運行並且顯示未移動的內容,內容也會自動取消隱藏。
使用此技術可能會影響其他指標(特別是 LCP 和 First Contentful Paint),因為您正在延遲渲染,並且還可能阻止瀏覽器的前瞻預加載器,但對於不存在其他選項的情況,它是另一個需要考慮的工具。
結論
Cumulative Layout Shift 是由內容更改尺寸或通過延遲運行的 JavaScript 將新內容注入頁面引起的。 在這篇文章中,我們討論了避免這種情況的各種提示和技巧。 我很高興 Core Web Vitals 將焦點放在了這個惱人的問題上——長期以來,我們 Web 開發人員(當然我自己也包括在內)忽視了這個問題。
清理我自己的網站為所有訪問者帶來了更好的體驗。 我鼓勵你也看看你的 CLS 問題,希望這些技巧中的一些在你這樣做時會有用。 誰知道呢,您甚至可能設法將所有頁面的 CLS 分數降到難以捉摸的 0 分!
更多資源
- Smashing Magazine 上的 Core Web Vitals 文章,包括我自己關於設置圖像寬度和高度、測量核心 Web Vitals 和 CSS 字體描述符的文章。
- Google 的 Core Web Vitals 文檔,包括他們在 CLS 上的頁面。
- 有關 CLS 最近更改的更多詳細信息,然後此更改開始在各種 Google 工具中更新。
- CLS 更改日誌詳細說明了每個版本的 Chrome 中的更改。
- Jess Peck 的幾乎完整的累積佈局轉換指南。
- Cumulative Layout Shift: Measure and Avoid Visual Instability by Karolina Szczur。
- 一個 Layout Shift GIF 生成器,可幫助生成 CLS 的可共享演示。