使用 content-visibility 優化渲染效能

語言: CN / TW / HK

最近在業務中實際使用 content-visibility 進了一些渲染效能的優化。

這是一個比較新且有強大功能的屬性。本文將帶領大家深入理解一番。

何為 content-visibility

content-visibility :屬性控制一個元素是否渲染其內容,它允許使用者代理(瀏覽器)潛在地省略大量佈局和渲染工作,直到需要它為止。

MDN 原文:The content-visibility CSS property controls whether or not an element renders its contents at all, along with forcing a strong set of containments, allowing user agents to potentially omit large swathes of layout and rendering work until it becomes needed. Basically it enables the user agent to skip an element's rendering work (including layout and painting) until it is needed — which makes the initial page load much faster.

它有幾個常見的取值。

/* Keyword values */
content-visibility: visible;
content-visibility: hidden;
content-visibility: auto;

分別解釋一下:

  • content-visibility: visible :預設值,沒有任何效果,相當於沒有新增 content-visibility ,元素的渲染與往常一致。
  • content-visibility: hidden :與 display: none 類似,使用者代理將跳過其內容的渲染。(這裡需要注意的是,跳過的是內容的渲染)
  • content-visibility: auto :如果該元素不在螢幕上,並且與使用者無關,則不會渲染其後代元素。

contain-intrinsic-size

當然,除 content-visibility 之外,還有一個與之配套的屬性 -- contain-intrinsic-size

contain-intrinsic-size :控制由 content-visibility 指定的元素的自然大小。

上面兩個屬性光看定義和介紹會有點繞。

我們首先來看看 content-visibility 如何具體使用。

content-visibility: visible 是預設值,新增後沒有任何效果,我們就直接跳過。

利用 content-visibility: hidden 優化展示切換效能

首先來看看 content-visibility: hidden ,它通常會拿來和 display: none 做比較,但是其實它們之間還是有很大的不同的。

首先,假設我們有兩個 DIV 包裹框:

<div class="g-wrap">
<div>1111</div>
<div class="hidden">2222</div>
</div>

設定兩個 div 為 200x200 的黑色塊:

.g-wrap > div {
width: 200px;
height: 200px;
background: #000;
}

效果如下:

OK,沒有問題,接下來,我們給其中的 .hidden 設定 content-visibility: hidden ,看看會發生什麼:

.hidden {
content-visibility: hidden;
}

效果如下:

注意,仔細看效果,這裡添加了 content-visibility: hidden 之後, 消失的只是添加了該元素的 div 的子元素消失不見,而父元素本身及其樣式,還是存在頁面上的

如果我們去掉設定了 content-visibility: hidden 的元素本身的 widthheightpaddingmargin 等屬性,則元素看上去就如同設定了 display: none 一般,在頁面上消失不見了。

那麼, content-visibility: hidden 的作用是什麼呢?

設定了 content-visibility: hidden 的元素, 其元素的子元素將被隱藏,但是,它的渲染狀態將會被快取 。所以,當 content-visibility: hidden 被移除時,使用者代理無需重頭開始渲染它和它的子元素。

因此,如果我們將這個屬性應用在一些一開始需要被隱藏,但是其後在頁面的某一時刻需要被渲染,或者是一些需要被頻繁切換顯示、隱藏狀態的元素上,其渲染效率將會有一個非常大的提升。

利用 content-visibility: auto 實現懶載入或虛擬列表

OK,接下來是 content-visibility 的核心用法,利用 auto 屬性值。

content-visibility: auto 的作用是,如果該元素不在螢幕上,並且與使用者無關,則不會渲染其後代元素。是不是與 LazyLoad 非常類似?

我們來看這樣一個 DEMO ,瞭解其作用:

假設,我們存在這樣一個 HTML 結構,含有大量的文字內容:

<div class="g-wrap">
<div class="paragraph">...</div>
// ... 包含了 N 個 paragraph
<div class="paragraph">...</div>
</div>

每個 .paragraph 的內容如下:

因此,整個的頁面看起來就是這樣的:

由於,我們沒有對頁面內容進行任何處理,因此,所有的 .paragraph 在頁面重新整理的一瞬間,都會進行渲染,看到的效果就如上所示。

當然,現代瀏覽器愈加趨於智慧,基於這種場景,其實我們非常希望對於仍未看到,仍舊未滾動到的區域,可以延遲載入,只有到我們需要展示、滾動到該處時,頁面內容才進行渲染。

基於這種場景, content-visibility: auto 就應運而生了,它允許瀏覽器對於設定了該屬性的元素進行判斷,如果該元素當前不處於視口內,則不渲染該元素。

我們基於上述的程式碼,只需要最小化,新增這樣一段程式碼:

.paragraph {
content-visibility: auto;
}

再看看效果,仔細觀察右側的滾動條:

這裡我使用了 ::-webkit-scrollbar 相關樣式,讓滾動條更明顯。

可能你還沒意識到發生了什麼,我們對比下添加了 content-visibility: auto 和沒有新增 content-visibility: auto 的兩種效果下文字的整體高度:

有著非常明顯的差異,這是因為,設定了 content-visibility: auto 的元素,在非可視區域內,目前並沒有被渲染,因此,右側內容的高度其實是比正常狀態下少了一大截的。

好,我們實際開始進行滾動,看看會發生什麼:

由於下方的元素在滾動的過程中,出現在視口範圍內才被渲染,因此,滾動條出現了明顯的飄忽不定的抖動現象。(當然這也是使用了 content-visibility: auto 的一個小問題之一),不過明顯可以看出,這與我們通常使用 JavaScript 實現的懶載入或者延遲載入非常類似。

當然,與懶載入不同的是,在向下滾動的過程中,上方消失的已經被渲染過且消失在視口的元素,也會因為消失在視口中,重新被隱藏。因此,即便頁面滾動到最下方,整體的滾動條高度還是沒有什麼變化的。

content-visibility 是否能夠優化渲染效能?

那麼, content-visibility 是否能夠優化渲染效能呢?

Youtube -- Slashing layout cost with content-visibility [1] 中,給了一個非常好的例子。

這裡我簡單復現一下。

對於一個存在巨量 HTML 內容的頁面,譬如類似於這個頁面 -- HTML - Living Standard [2]

可以感受到,往下翻,根本翻不到盡頭。(這裡我在本地模擬了該頁面,複製了該頁面的所有 DOM,並非實際在該網站進行測試)

如果不對這個頁面做任何處理,看看首次渲染需要花費的時間:

可以看到,DOMContentLoaded 的時間的 3s+ ,而花費在 Rendering 上的就有整整 2900ms

而如果給這個頁面的每個段落,新增上 content-visibility: auto ,再看看整體的耗時:

可以看到,DOMContentLoaded 的時間驟降至了 500ms+ ,而花費在 Rendering 上的,直接優化到了 61ms

2900ms --> 61ms,可謂是驚人級別的優化了。因此, content-visibility: auto 對於長文字、長列表功能的優化是顯而易見的。

利用 contain-intrinsic-size 解決滾動條抖動問題

當然, content-visibility 也存在一些小問題。

從上面的例子,也能看到,在利用 content-visibility: auto 處理長文字、長列表的時候。在滾動頁面的過程中,滾動條一直在抖動,這不是一個很好的體驗。

當然,這也是許多虛擬列表都會存在的一些問題。

好在,規範制定者也發現了這個問題。這裡我們可以使用另外一個 CSS 屬性,也就是文章一開頭提到的另外一個屬性 -- contain-intrinsic-size ,來解決這個問題。

contain-intrinsic-size :控制由 content-visibility 指定的元素的自然大小。

什麼意思呢?

還是上面的例子

<div class="g-wrap">
<div class="paragraph">...</div>
// ... 包含了 N 個 paragraph
<div class="paragraph">...</div>
</div>

如果我們不使用 contain-intrinsic-size ,只對視口之外的元素使用 content-visibility: auto ,那麼視口外的元素高度通常就為 0。

當然,如果直接給父元素設定固定的 height ,也是會有高度的。

那麼實際的滾動效果,滾動條就是抖動的:

所以,我們可以同時利用上 contain-intrinsic-size ,如果能準確知道設定了 content-visibility: auto 的元素在渲染狀態下的高度,就填寫對應的高度。如果如法準確知道高度,也可以填寫一個大概的值:

.paragraph {
content-visibility: auto;
contain-intrinsic-size: 320px;
}

如此之後,瀏覽器會給未被實際渲染的視口之外的 .paragraph 元素一個高度,避免出現滾動條抖動的現象:

你可以自己親自嘗試感受一下: CodePen Demo -- content-visibility: auto Demo [3]

content-visibility 的一些其他問題

首先,看看 content-visibility 的相容性(2022-06-03):

目前還是比較慘淡的,並且我沒有實際在業務中使用它,需要再等待一段時間。 當然,由於該屬性屬於漸進增強一類的功能,即便失效,也完全不影響頁面本身的展示。

同時,也有一些同學表示,利用 content-visibility: auto 只能解決部分場景,在海量 DOM 的場景下的實際效果,還有待進一步的實測。真正運用的時候,多做對比,再做取捨。

當然,現代瀏覽器已經越來越智慧,類似 content-visibility 功能的屬性也越來越多,我們在效能優化的路上有了更多選擇,總歸是一件好事。

最後

本文到此結束,希望對你有幫助 :)

想 Get 到最有意思的 CSS 資訊,千萬不要錯過我的公眾號 -- iCSS前端趣聞 :smile:

更多精彩 CSS 技術文章彙總在我的 Github -- iCSS [4] ,持續更新,歡迎點個 star 訂閱收藏。

如果還有什麼疑問或者建議,可以多多交流,原創文章,文筆有限,才疏學淺,文中若有不正之處,萬望告知。

參考資料

[1]

Youtube -- Slashing layout cost with content-visibility: https://www.youtube.com/watch?v=FFA-v-CIxJQ&t=869s

[2]

HTML - Living Standard: https://html.spec.whatwg.org/

[3]

CodePen Demo -- content-visibility: auto Demo: https://codepen.io/Chokcoco/pen/rNJvPEX

[4]

Github -- iCSS: https://github.com/chokcoco/iCSS

iCSS,不止於 CSS,如果你也對各種新奇有趣的前端(CSS)知識感興趣,歡迎關注 。同時如果你有任何想法疑問,或者也想入群參與大前端技術討論,圍觀答疑解惑,共同成長進步,可以關注公眾號 加我微信,拉你入群

因為微信公眾號修改規則,如果不標星或點在看,你可能會收不到我公眾號文章的推送,請大家將本 公眾號星標 ,看完文章後記得 點下贊 或者 在看 ,謝謝各位!