渲染優化之CSS Containment

袋鼠雲數棧前端發表於2021-08-19

引言

在開始介紹今天的主角 CSS Containment 之前,我們需要了解一些前置知識迴流和重繪,方便我們理解以及應用的場景。

簡單回憶下回流和重繪

  • 迴流(Reflow):當瀏覽器必須重新處理和繪製部分或全部頁面時,迴流就會發生,例如元素的規模尺寸,佈局,隱藏等改變而需要重新構建。
  • 重繪(Repaint):當改變元素的部分屬性而不影響佈局時,重繪就會發生。例如改變元素的背景顏色、字型顏色等。

迴流會造成什麼

Reflows are very expensive in terms of performance, and is one of the main causes of slow DOM scripts, In many cases, they are equivalent to laying out the entire page again.

通過翻譯,我們可以知道,迴流在效能方面消耗非常大,是很多 DOM 載入慢的原因之一。在許多情況下,它們相當於再次渲染整個頁面。

接下來,來看看有哪些行為會觸發迴流/重繪。

觸發迴流/重繪

  • 新增,刪除,更新 DOM 節點時會發生迴流
  • 設定元素的屬性為display:none 時發生迴流
  • 設定元素的屬性visibility: hidden 時發生重繪
  • DOM 節點上存在動畫屬性也將觸發迴流
  • 調整視窗的大小將觸發迴流
  • font-style 更改字型風格會改變元素的幾何形狀。 這意味著它可能會影響頁面上其他元素的位置或大小,觸發迴流
  • 新增或刪除樣式檔案將導致迴流/重繪
  • 通過 JavaScript 獲取元素的大小等,由於需要確保獲取到的值為最新的,瀏覽器都會先執行一次迴流來保證值的正確。例如 offsetXXXclientXXXscrollXXX

重繪迴流優化方案

知道了觸發迴流/重繪的原因,那麼就能根據這些原因,制定相應的優化方案,如下。

  • 避免使用觸發重繪迴流的 CSS 屬性。
  • 儘量減少 JS 操作修改 DOM 的 CSS 次數。
  • 將頻繁重繪迴流的 DOM 元素單獨作為一個獨立圖層,那麼這個 DOM 元素的重繪和迴流影響只會在這個圖層中。

經過了優化後,迴流和重繪的次數已經減少,但是不可避免的,由於各種原因,還是會產生迴流和重繪。

試想一下,有一個比較複雜的頁面,當使用者移動滑鼠到一個元素上,觸發這個元素hover,這個hover的效果是使這個元素寬高發生改變(widthheight),當元素的寬高發生改變時,瀏覽器需要考慮到所有元素,是否發生了相應的更改,所以瀏覽器需要對整個頁面進行重新佈局,而實際上改變的可能只有頁面的一小部分,頁面大部分內容是保持不變的。這對於效能來說,無疑是十分差的。

那麼有沒有一種辦法,能夠讓瀏覽器進行區域性的迴流重繪,從而達到優化效能的目的呢?或者說,減少迴流時產生的效能消耗。答案是有的,就是今天所要認識的 CSS Containment

CSS Containment

CSS Containment 主要是通過允許開發者將某些子樹從頁面中獨立出來,從而提高頁面的效能。如果瀏覽器知道頁面中的某部分是獨立的,就能夠優化渲染並獲得效能提升。

由於有很多的互動或者複雜的情況,需要觸發迴流,重新渲染整個頁面。為了改進這個,瀏覽器必須識別有哪些部分是獨立的。當他們的子元素有變化時,瀏覽器的渲染引擎能夠識別到,只對部分元素做迴流重繪,而不對整個頁面進行。

識別這個標準的屬性就是 contain

contain

通過 contain 屬性告訴瀏覽器,這些節點是獨立的。

語法

div {
  contain: none; /* 表示元素將正常渲染,沒有包含規則 */
  contain: layout; /* 表示元素外部無法影響元素內部的佈局,反之亦然 */
  contain: paint; /* 表示這個元素的子孫節點不會在它邊緣外顯示。如果一個元素在視窗外或因其他原因導致不可見,則同樣保證它的子孫節點不會被顯示。 */
  contain: size; /* 表示這個元素的尺寸計算不依賴於它的子孫元素的尺寸 */
  
  contain: content; /* 等價於 contain: layout paint */
  contain: strict; /* 等價於 contain: size layout paint */
}

一個例子

Layout

This value turns on layout containment for the element. This ensures that the containment box is totally opaque for layout purposes; nothing outside can affect its internal layout, and vice versa.

設定了 layout 屬性,就是告訴瀏覽器當前元素內部的樣式變化不會引起元素外部的樣式變化。並且,元素外部的樣式變化也不會引起元素內部的樣式變化。這樣,瀏覽器就可以相應的減少渲染元素,提高渲染的效能。

如果設定了 layout 屬性的元素,被遮擋,如螢幕外。則瀏覽器會把該元素相關的處理,放到較低的優先順序中。

.container li {
    padding: 10px;
    height: 100px;
    
    contain: layout;
}

file

值得注意的是,由於元素內部的樣式變化,導致了元素本身發生了大小等能觸發迴流的屬性時,那麼 layout 屬性將不生效。

Paint

This value turns on paint containment for the element. This ensures that the descendants of the containment box don’t display outside its bounds, so if an element is off-screen or otherwise not visible, its descendants are also guaranteed to be not visible.

設定了 paint 屬性,表示這個元素的子孫節點不會在它邊緣外顯示。如果一個元素在視窗外或因其他原因導致不可見,則同樣它的子孫節點不會被顯示。

.container li {
    padding: 10px;
    height: 100px;
    
    contain: paint;
}

file

對於子元素,部分內容超出邊界,那麼該部分內容也不會被渲染。

從效果上來看,這有點類似於 overflow:hidden,不同的是 overflow:hidden,是通過將超出部分進行裁剪的方式。

舉個例子,對於有滾動條的元素,由於滾動,會觸發多次渲染,這些渲染的元素,包含當前可視區外的元素,造成了效能浪費。而使用 paint 就可以忽略這些可視區外元素的渲染,從而達到優化渲染效能。

Size

The value turns on size containment for the element. This ensures that the containment box can be laid out without needing to examine its descendants.

設定了 size 屬性的元素,表示這個元素的尺寸計算不依賴於它的子孫元素的尺寸。

對於瀏覽器來說,設定 size 就是告訴瀏覽器,這個元素的大小已經固定了,就是這麼大,不需要再通過重排子元素來獲取當前元素的大小。

設定了 size 屬性的元素,不管子元素是怎麼佈局,什麼樣式,都不會影響到父元素。

.container li {
    padding: 10px;
    height: 100px;
    
    contain: size;
}

file

使用這個 size 屬性,會改變渲染的根結點,從而達到優化的目的

使用前:

file

使用後:

file

可以看到,layout root 是完全不同的,前者基於 document 整個頁面,而後者是基於當前的 contain 容器元素。

在日常使用中,我們可以對一些容器元素使用,避免因為容器內部的佈局改變,而導致整個頁面的迴流。

content && strict

contain:content; // 表示這個元素上有除了 size 和 style 外的所有包含規則。等價於 contain: layout paint。

contain:strict; // 表示除了 style 外的所有的包含規則應用於這個元素。等價於 contain: size layout paint。

佈局

不知道大家是否注意到,設定了contain的元素,只有在明確了width, height的情況下,才會產生效果,否則就跟正常元素一樣。

真的沒有其他任何變化麼?其實不是的。

只要設定了contain的元素,就類似於使用 position:relative 佈局,不同的是,z-index,以及topleft等改變位置的屬性對其自身是無效。

對於設定contain: layout,通過觀察可以看到,觀感上它與 position:relative 並無區別,都是在正常文件流中佔據位置,且子元素浮於正常文件流之上。

file

但是,對於設定contain: size的元素,通過觀察可以看到,它也是在正常文件流中佔據位置,不同的是,子元素浮於正常文件流之下,這就可以說明,只要設定了contain: size,它的層級是低於正常文件流的。

file

example

為了更直觀的看出 contain 的效果,先附上 Manuel Rego Casasnovas 寫的例子。

window.performance.now() // 返回一個表示從效能測量時刻開始經過的毫秒數

通過[window.performance.now()](https://developer.mozilla.org/zh-CN/docs/Web/API/Performance/now)記錄迴流的開始時間,在迴流結束後再通過[window.performance.now()](https://developer.mozilla.org/zh-CN/docs/Web/API/Performance/now)記錄一次結束時間,用得到的開始時間和結束時間相減,就得到了一次完整迴流所經歷的時間。

function runTests() {
    setup(); // 建立 1000 個節點

    let avg1 = changeTargetContent(); // 沒有設定contain,觸發迴流

    let targetItem = document.getElementById('targetItem');
    targetItem.style.contain = 'strict';
    let avg2 = changeTargetContent(); // 觸發迴流
}

function changeTargetContent() {
    // Force layout.
    document.body.offsetLeft;

    let start = window.performance.now();

    let targetInner = document.getElementById('targetInner');
    targetInner.textContent =
        targetInner.textContent == 'Hello World!'
            ? 'BYE'
            : 'Hello World!';

    // Force layout.
    document.body.offsetLeft;

    let end = window.performance.now();
    let time =end - start;
    return time;
}

file

通過對比cantain: strict設定前和設定後,可以看到效能的優化達到了 80%左右。

在實際專案裡下,使用cantain: strict屬性後的效果。

截圖場景,點選了 2 次按鈕,完整觸發了一個模組的開啟關閉,前者為使用前,後者為使用後的的實際渲染效果。

使用前:
file

使用後:
file

通過比較,可以看出使用 cantain: strict 後,rendering 時長從 1750ms 降至 558ms,優化了 60% 左右。而 painting 時長從 230ms 降至 35ms,優化了 75% 的左右。

rendering 和 Painting 的佔用時間,都有非常明顯的減少。使用後對渲染效能的優化還是非常明顯的。

相容性

file

寫在最後

在本次的學習中,其實還有一些值得探究或者比較遺憾的地方:

  • contain在優化頁面渲染效能的情況下,是否給瀏覽器帶來了其他負擔?個人猜測是通過空間換時間的方式。
  • 設計的 demo 的實際效果跟理想中的效果,並不一致,不免有些遺憾。如對於 contain:paint 來說,在螢幕外新增子節點,觸發迴流重繪,根據contain:paint屬性在螢幕外,不繪製元素的特性,重繪的時間應該是非常小,或者將近 0ms 的,然而在實際中並沒有達到這個效果。

如果文章中出現錯誤,或者有更好的驗證 demo,歡迎留言交流哈?。

參考文獻

相關文章