上週把「無規格解」四個字貼在 content-visibility: auto 的 CLS 問題上,今天回頭查證,發現這個講法一半對一半錯——對的一半是「殘留的位移確實不會被排除在計算之外」,錯的一半是「規格與瀏覽器根本沒有處理過這件事」,實際上有一輪明確的規格決議與瀏覽器改動,只是解法的作用範圍比想像中窄。

📖 學

問題的起點很單純。content-visibility: auto 讓瀏覽器對畫面外的內容跳過 layout 與 paint,省下渲染成本;但當這段內容捲回可視範圍,瀏覽器必須「取消跳過」(unskip)並真正排版一次,這次排版通常會讓元素從「空的/佔位大小」變成「實際內容大小」,產生一次真實的尺寸跳動。2021 年 1 月 11 日,GitHub 使用者 zeemawn 在 WICG/layout-instability#89 回報,光是捲動頁面就能讓量到的 CLS 持續累加、輕鬆超過 1.0,即使那些位移根本發生在可視範圍之外、使用者完全看不到。

Chrome 88(2020 年 11 月的變更、隨版號在 2021 年初出貨)對此做了修正:content-visibility: auto 元素自身因 unskip 而產生的那次 resize,不再計入 CLS。但 Chromium 的 CLS metrics changelog 文件(2020_11_cls.md)明確寫著修正的邊界——與該元素相鄰(adjacent)但不是其子孫(descendant)的畫面內元素,如果因為這次 resize 被推擠而位移,這個位移依然要算進 CLS。換句話說,豁免的範圍只到這個元素自己的盒子為止,不包含它旁邊被擠開的東西。這不是一個「還沒修好的 bug」,而是修正案本來就只打算解決自己這一格的問題。

真正處理「旁邊元素被擠開」這件事的,是另一條完全不同的路徑:把 unskip 時的尺寸落差本身縮小。長期以來,開發者被建議手動搭配 contain-intrinsic-size 給一個估計值當佔位大小,估得越準,unskip 時的跳動(連帶波及鄰居的位移)就越小。但手動估計終究是猜。2023 年 7 月 14 日,Google 的 Vladimir Levin(vmpstr@chromium.org)在 blink-dev 提出 Intent to Prototype and Ship:讓 content-visibility: auto 自動隱含 contain-intrinsic-size: auto,不需要開發者手寫。auto 關鍵字的效果是瀏覽器記住元素上一次「真正渲染」時的實際尺寸,下次跳過內容時就拿這個記住的尺寸當佔位大小,而不是隨便一個固定值。7 月 25 日三位 API owner(Alex Russell、Chris Harrelson、Yoav Weiss)相繼 LGTM,目標版本是 Chrome 117。同一篇討論裡也提到一個容易被忽略的事實:當時 Gecko(Firefox 的引擎)已經先出貨了這個行為,反而是 WebKit 當時「no signal」,還沒表態。

跨瀏覽器的時間軸拉出來看,content-visibility 整體在 MDN 的頁面上標成 Baseline 2024 newly available:Chrome 85(2020 年 8 月)、Firefox 125(2024 年 4 月 16 日出貨,對應 bug 1874874)、Safari 18.0(2024 年 9 月)。但「記住尺寸」這個關鍵的 contain-intrinsic-size: auto 支援,矩陣要湊齊還得再等——Safari 是到 18.1(2024 年 10 月 28 日發布)才補上,也就是說 Safari 18.0 雖然支援了 content-visibility: auto 本身,卻晚了整整一個小版號才有「記住尺寸」這張安全網,這段空窗期裡 Safari 使用者理論上會看到比 Chrome/Firefox 更大的 unskip 跳動。

已標存疑之處在於:即使「記住尺寸」機制全面到位,它本身也還有沒收乾淨的角落。csswg-drafts#7807 描述的情境是:當 contain-intrinsic-size: auto 100px 搭配 content-visibility: auto 的元素又被顯式加上 contain: size 時,尺寸容器化生效期間「記住的尺寸」不會更新,於是捲出畫面外時退回舊的記住值(例如 15px)、捲回來時又跳回容器強制的 100px,產生反覆的 scrollbar jitter。這個 issue 標著「Closed Accepted by CSSWG Resolution」但同時掛著「Needs Edits」,也就是工作小組已經同意要修,規格文字本身還沒編輯完成,目前查不到明確的定案日期,只能標記存疑。另外 WebKit 這邊也有一筆獨立回報的迴歸:bug 277573 指出 Safari 18 裡 details/summary 元素一旦套上 content-visibility: auto 就展開不了——同樣掛著 Baseline 標籤的功能,實作細節仍然各自有坑。

🧠 記

content-visibility: auto 對 CLS 的影響要拆成三層看,不能一句「有解」或「無解」帶過:①豁免範圍只到元素自己的盒子(Chrome 88 起),旁邊被擠開的鄰居位移從來沒被排除在計算外,這是設計邊界不是遺留 bug;②真正的緩解手段是「記住上次渲染尺寸」讓 unskip 落差變小,靠的是 content-visibility: auto 自動隱含 contain-intrinsic-size: auto(Chrome 117 起、Gecko 更早、Safari 18.1 才補齊),但這只是把跳動變小,不是變不存在;③這個緩解機制自身在跟 contain: size 顯式疊加時仍有未收尾的規格角落(csswg-drafts#7807),加上 WebKit 有獨立的實作迴歸,代表「Baseline 2024」這個標籤蓋住了瀏覽器間仍然存在的行為落差。

✍️ 實踐

  1. 在 DevTools 的 Rendering 分頁打開 Layout Shift Regions,對一個用了 content-visibility: auto 的清單頁面反覆捲動,觀察高亮的位移區域是落在該元素自己身上,還是它下方/旁邊的相鄰元素——後者就是仍然計入 CLS、且無法靠豁免規則解決的那一種。
  2. 在 Elements 面板檢查該元素的 Computed 樣式,找 contain-intrinsic-size 的實際計算值,對照它真正渲染出來的高度,量一下兩者差距——差距越大,unskip 時的位移(以及波及鄰居的量)就越大。
  3. 用 PerformanceObserver 訂閱 layout-shift entry,印出每筆的 sources,確認觸發位移的節點究竟是 content-visibility: auto 元素本身還是它的鄰居,別急著把責任全推給這個屬性。
  4. 如果專案支援矩陣涵蓋 Safari,額外用 18.0 與 18.1(以上)分別測一次同一個頁面的 CLS,確認差異是否真的來自「記住尺寸」機制的支援時間差,而不是別的原因。

🔗 延伸學習

💬 問 AI

我在讀一篇談 content-visibility:auto 對 Cumulative Layout Shift 影響的文章,
內容分成三層:①Chrome 88 起,元素自己因 unskip 產生的位移不計入 CLS,但相鄰元素被
擠開的位移仍然計入;②Chrome 117 起 content-visibility:auto 會自動隱含
contain-intrinsic-size:auto,讓瀏覽器記住上次真實渲染尺寸來縮小 unskip 落差,
Safari 要到 18.1(2024-10-28)才補齊;③這個「記住尺寸」機制搭配顯式 contain:size
時仍有 csswg-drafts#7807 描述的邊界案例未完全收尾。
請幫我用我們專案裡實際使用 content-visibility:auto 的頁面(告訴我對應檔案路徑),
檢查是否同時設了 contain-intrinsic-size,並評估是否可能踩到 #7807 那種
「記住尺寸不更新」的情境。