引言
在開始介紹今天的主角 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 獲取元素的大小等,由於需要確保獲取到的值為最新的,瀏覽器都會先執行一次回流來保證值的正確。例如 offsetXXX、clientXXX 和 scrollXXX 等
重繪回流優化方案
知道了觸發回流/重繪的原因,那么就能根據這些原因,制定相應的優化方案,如下。
- 避免使用觸發重繪回流的 CSS 屬性。
- 盡量減少 JS 操作修改 DOM 的 CSS 次數。
- 將頻繁重繪回流的 DOM 元素單獨作為一個獨立圖層,那么這個 DOM 元素的重繪和回流影響只會在這個圖層中。
經過了優化后,回流和重繪的次數已經減少,但是不可避免的,由於各種原因,還是會產生回流和重繪。
試想一下,有一個比較復雜的頁面,當用戶移動鼠標到一個元素上,觸發這個元素hover
,這個hover
的效果是使這個元素寬高發生改變(width
、height
),當元素的寬高發生改變時,瀏覽器需要考慮到所有元素,是否發生了相應的更改,所以瀏覽器需要對整個頁面進行重新布局,而實際上改變的可能只有頁面的一小部分,頁面大部分內容是保持不變的。這對於性能來說,無疑是十分差的。
那么有沒有一種辦法,能夠讓瀏覽器進行局部的回流重繪,從而達到優化性能的目的呢?或者說,減少回流時產生的性能消耗。答案是有的,就是今天所要認識的 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;
}
值得注意的是,由於元素內部的樣式變化,導致了元素本身發生了大小等能觸發回流的屬性時,那么 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;
}
對於子元素,部分內容超出邊界,那么該部分內容也不會被渲染。
從效果上來看,這有點類似於 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;
}
使用這個 size 屬性,會改變渲染的根結點,從而達到優化的目的
使用前:
使用后:
可以看到,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
,以及top
、left
等改變位置的屬性對其自身是無效。
對於設置contain: layout
,通過觀察可以看到,觀感上它與 position:relative
並無區別,都是在正常文檔流中占據位置,且子元素浮於正常文檔流之上。
但是,對於設置contain: size
的元素,通過觀察可以看到,它也是在正常文檔流中占據位置,不同的是,子元素浮於正常文檔流之下,這就可以說明,只要設置了contain: size
,它的層級是低於正常文檔流的。
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;
}
通過對比cantain: strict
設置前和設置后,可以看到性能的優化達到了 80%左右。
在實際項目里下,使用cantain: strict
屬性后的效果。
截圖場景,點擊了 2 次按鈕,完整觸發了一個模塊的打開關閉,前者為使用前,后者為使用后的的實際渲染效果。
使用前:
使用后:
通過比較,可以看出使用 cantain: strict
后,rendering 時長從 1750ms 降至 558ms,優化了 60% 左右。而 painting 時長從 230ms 降至 35ms,優化了 75% 的左右。
rendering 和 Painting 的占用時間,都有非常明顯的減少。使用后對渲染性能的優化還是非常明顯的。
兼容性
寫在最后
在本次的學習中,其實還有一些值得探究或者比較遺憾的地方:
contain
在優化頁面渲染性能的情況下,是否給瀏覽器帶來了其他負擔?個人猜測是通過空間換時間的方式。- 設計的 demo 的實際效果跟理想中的效果,並不一致,不免有些遺憾。如對於
contain:paint
來說,在屏幕外添加子節點,觸發回流重繪,根據contain:paint
屬性在屏幕外,不繪制元素的特性,重繪的時間應該是非常小,或者將近 0ms 的,然而在實際中並沒有達到這個效果。
如果文章中出現錯誤,或者有更好的驗證 demo,歡迎留言交流哈😊。