【優(yōu)化】790- 網(wǎng)頁渲染性能優(yōu)化 —— 渲染原理

作者:晨風(fēng)明悟
鏈接:https://zhuanlan.zhihu.com/p/39879808
渲染原理
在討論性能優(yōu)化之前,我們有必要了解一些瀏覽器的渲染原理。不同的瀏覽器進(jìn)行渲染有著不同的實(shí)現(xiàn)方式,但是大體流程都是差不多的,我們通過 Chrome 瀏覽器來大致了解一下這個(gè)渲染流程。

關(guān)鍵渲染路徑
關(guān)鍵渲染路徑是指瀏覽器將 HTML、CSS 和 JavaScript 轉(zhuǎn)換成實(shí)際運(yùn)作的網(wǎng)站必須采取的一系列步驟,通過渲染流程圖我們可以大致概括如下:
- 處理 HTML 并構(gòu)建 DOM Tree。
- 處理 CSS 并構(gòu)建 CSSOM Tree。
- 將 DOM Tree 和 CSSOM Tree 合并成 Render Object Tree。
- 根據(jù) Render Object Tree 計(jì)算節(jié)點(diǎn)的幾何信息并以此進(jìn)行布局。
- 繪制頁面需要先構(gòu)建 Render Layer Tree 以便用正確的順序展示頁面,這棵樹的生成與 Render Object Tree 的構(gòu)建同步進(jìn)行。然后還要構(gòu)建 Graphics Layer Tree 來避免不必要的繪制和使用硬件加速渲染,最終才能在屏幕上展示頁面。
DOM Tree
DOM(Document Object Model——文檔對(duì)象模型)是用來呈現(xiàn)以及與任意 HTML 或 XML 交互的 API 文檔。DOM 是載入到瀏覽器中的文檔模型,它用節(jié)點(diǎn)樹的形式來表現(xiàn)文檔,每個(gè)節(jié)點(diǎn)代表文檔的構(gòu)成部分。
需要說明的是 DOM 只是構(gòu)建了文檔標(biāo)記的屬性和關(guān)系,并沒有說明元素需要呈現(xiàn)的樣式,這需要 CSSOM 來處理。
構(gòu)建流程
獲取到 HTML 字節(jié)數(shù)據(jù)后,會(huì)通過以下流程構(gòu)建 DOM Tree:

- 編碼:HTML 原始字節(jié)數(shù)據(jù)轉(zhuǎn)換為文件指定編碼的字符串。
- 詞法分析(標(biāo)記化):對(duì)輸入字符串進(jìn)行逐字掃描,根據(jù) 構(gòu)詞規(guī)則 識(shí)別單詞和符號(hào),分割成一個(gè)個(gè)我們可以理解的詞匯(學(xué)名叫 Token )的過程。
- 語法分析(解析器):對(duì) Tokens 應(yīng)用 HTML 的語法規(guī)則,進(jìn)行配對(duì)標(biāo)記、確立節(jié)點(diǎn)關(guān)系和綁定屬性等操作,從而構(gòu)建 DOM Tree 的過程。
詞法分析和語法分析在每次處理 HTML 字符串時(shí)都會(huì)執(zhí)行這個(gè)過程,比如使用 document.write 方法。

詞法分析(標(biāo)記化)
HTML 結(jié)構(gòu)不算太復(fù)雜,大部分情況下識(shí)別的標(biāo)記會(huì)有開始標(biāo)記、內(nèi)容標(biāo)記和結(jié)束標(biāo)記,對(duì)應(yīng)一個(gè) HTML 元素。除此之外還有 DOCTYPE、Comment、EndOfFile 等標(biāo)記。
標(biāo)記化是通過狀態(tài)機(jī)來實(shí)現(xiàn)的,狀態(tài)機(jī)模型在 W3C 中已經(jīng)定義好了。
想要得到一個(gè)標(biāo)記,必須要經(jīng)歷一些狀態(tài),才能完成解析。我們通過一個(gè)簡單的例子來了解一下流程。
W3C

- 開始標(biāo)記:
- Data state:碰到 <,進(jìn)入 Tag open state
- Tag open state:碰到 a,進(jìn)入 Tag name state 狀態(tài)
- Tag name state:碰到 空格,進(jìn)入 Before attribute name state
- Before attribute name state:碰到 h,進(jìn)入 Attribute name state
- Attribute name state:碰到 =,進(jìn)入 Before attribute value state
- Before attribute value state:碰到 ",進(jìn)入 Attribute value (double-quoted) state
- Attribute value (double-quoted) state:碰到 w,保持當(dāng)前狀態(tài)
- Attribute value (double-quoted) state:碰到 ",進(jìn)入 After attribute value (quoted) state
- After attribute value (quoted) state:碰到 >,進(jìn)入 Data state,完成解析
- 內(nèi)容標(biāo)記:W3C
- Data state:碰到 W,保持當(dāng)前狀態(tài),提取內(nèi)容
- Data state:碰到 <,進(jìn)入 Tag open state,完成解析
- 結(jié)束標(biāo)記:
- Tag open state:碰到 /,進(jìn)入 End tag open state
- End tag open state:碰到 a,進(jìn)入 Tag name state
- Tag name state:碰到 >,進(jìn)入 Data state,完成解析
通過上面這個(gè)例子,可以發(fā)現(xiàn)屬性是開始標(biāo)記的一部分。
語法分析(解析器)
在創(chuàng)建解析器后,會(huì)關(guān)聯(lián)一個(gè) Document 對(duì)象作為根節(jié)點(diǎn)。
我會(huì)簡單介紹一下流程,具體的實(shí)現(xiàn)過程可以在 Tree construction 查看。
解析器在運(yùn)行過程中,會(huì)對(duì) Tokens 進(jìn)行迭代;并根據(jù)當(dāng)前 Token 的類型轉(zhuǎn)換到對(duì)應(yīng)的模式,再在當(dāng)前模式下處理 Token;此時(shí),如果 Token 是一個(gè)開始標(biāo)記,就會(huì)創(chuàng)建對(duì)應(yīng)的元素,添加到 DOM Tree 中,并壓入還未遇到結(jié)束標(biāo)記的開始標(biāo)記棧中;此棧的主要目的是實(shí)現(xiàn)瀏覽器的容錯(cuò)機(jī)制,糾正嵌套錯(cuò)誤,具體的策略在 W3C 中定義。更多標(biāo)記的處理可以在 狀態(tài)機(jī)算法 中查看。
參考資料
- 瀏覽器的工作原理:新式網(wǎng)絡(luò)瀏覽器幕后揭秘 —— 解析器和詞法分析器的組合
- 瀏覽器渲染過程與性能優(yōu)化 —— 構(gòu)建DOM樹與CSSOM樹
- 在瀏覽器的背后(一) —— HTML語言的詞法解析
- 在瀏覽器的背后(二) —— HTML語言的語法解析
- 50 行代碼的 HTML 編譯器
- AST解析基礎(chǔ): 如何寫一個(gè)簡單的html語法分析庫
- WebKit中的HTML詞法分析
- HTML文檔解析和DOM樹的構(gòu)建
- 從Chrome源碼看瀏覽器如何構(gòu)建DOM樹
- 構(gòu)建對(duì)象模型 —— 文檔對(duì)象模型 (DOM)
CSSOM Tree
加載
在構(gòu)建 DOM Tree 的過程中,如果遇到 link 標(biāo)記,瀏覽器就會(huì)立即發(fā)送請(qǐng)求獲取樣式文件。當(dāng)然我們也可以直接使用內(nèi)聯(lián)樣式或嵌入樣式,來減少請(qǐng)求;但是會(huì)失去模塊化和可維護(hù)性,并且像緩存和其他一些優(yōu)化措施也無效了,利大于弊,性價(jià)比實(shí)在太低了;除非是為了極致優(yōu)化首頁加載等操作,否則不推薦這樣做。
阻塞
CSS 的加載和解析并不會(huì)阻塞 DOM Tree 的構(gòu)建,因?yàn)?DOM Tree 和 CSSOM Tree 是兩棵相互獨(dú)立的樹結(jié)構(gòu)。但是這個(gè)過程會(huì)阻塞頁面渲染,也就是說在沒有處理完 CSS 之前,文檔是不會(huì)在頁面上顯示出來的,這個(gè)策略的好處在于頁面不會(huì)重復(fù)渲染;如果 DOM Tree 構(gòu)建完畢直接渲染,這時(shí)顯示的是一個(gè)原始的樣式,等待 CSSOM Tree 構(gòu)建完畢,再重新渲染又會(huì)突然變成另外一個(gè)模樣,除了開銷變大之外,用戶體驗(yàn)也是相當(dāng)差勁的。另外 link 標(biāo)記會(huì)阻塞 JavaScript 運(yùn)行,在這種情況下,DOM Tree 是不會(huì)繼續(xù)構(gòu)建的,因?yàn)?JavaScript 也會(huì)阻塞 DOM Tree 的構(gòu)建,這就會(huì)造成很長時(shí)間的白屏。
通過一個(gè)例子來更加詳細(xì)的說明:
性能
標(biāo)題
標(biāo)題2
首先需要在 Chrome 控制臺(tái)的 Network 面板設(shè)置網(wǎng)絡(luò)節(jié)流,讓網(wǎng)絡(luò)速度變慢,以便更好進(jìn)行調(diào)試。

下圖說明 JavaScript 的確需要在 CSS 加載并解析完畢之后才會(huì)執(zhí)行。

為什么需要阻塞 JavaScript 的運(yùn)行呢?
因?yàn)?JavaScript 可以操作 DOM 和 CSSOM,如果 link 標(biāo)記不阻塞 JavaScript 運(yùn)行,這時(shí) JavaScript 操作 CSSOM,就會(huì)發(fā)生沖突。更詳細(xì)的說明可以在 使用 JavaScript 添加交互 這篇文章中查閱。
解析
CSS 解析的步驟與 HTML 的解析是非常類似的。
詞法分析
CSS 會(huì)被拆分成如下一些標(biāo)記:

CSS 的色值使用十六進(jìn)制優(yōu)于函數(shù)形式的表示?
函數(shù)形式是需要再次計(jì)算的,在進(jìn)行詞法分析時(shí)會(huì)將它變成一個(gè)函數(shù)標(biāo)記,由此看來使用十六進(jìn)制的確有所優(yōu)化。

語法分析
每個(gè) CSS 文件或嵌入樣式都會(huì)對(duì)應(yīng)一個(gè) CSSStyleSheet 對(duì)象(authorStyleSheet),這個(gè)對(duì)象由一系列的 Rule(規(guī)則) 組成;每一條 Rule 都會(huì)包含 Selectors(選擇器) 和若干 Declearation(聲明),Declearation 又由 Property(屬性)和 Value(值)組成。另外,瀏覽器默認(rèn)樣式表(defaultStyleSheet)和用戶樣式表(UserStyleSheet)也會(huì)有對(duì)應(yīng)的 CSSStyleSheet 對(duì)象,因?yàn)樗鼈兌际菃为?dú)的 CSS 文件。至于內(nèi)聯(lián)樣式,在構(gòu)建 DOM Tree 的時(shí)候會(huì)直接解析成 Declearation 集合。

內(nèi)聯(lián)樣式和 authorStyleSheet 的區(qū)別
所有的 authorStyleSheet 都掛載在 document 節(jié)點(diǎn)上,我們可以在瀏覽器中通過 document.styleSheets 獲取到這個(gè)集合。內(nèi)聯(lián)樣式可以直接通過節(jié)點(diǎn)的 style 屬性查看。
通過一個(gè)例子,來了解下內(nèi)聯(lián)樣式和 authorStyleSheet 的區(qū)別:
Document
test
可以看到一共有三個(gè) CSSStyleSheet 對(duì)象,每個(gè) CSSStyleSheet 對(duì)象的 rules 里面會(huì)有一個(gè) CSSStyleDeclaration,而內(nèi)聯(lián)樣式獲取到的直接就是 CSSStyleDeclaration。

需要屬性合并嗎?
在解析 Declearation 時(shí)遇到屬性合并,會(huì)把單條聲明轉(zhuǎn)變成對(duì)應(yīng)的多條聲明,比如:
.box {
margin: 20px;
}
margin: 20px 就會(huì)被轉(zhuǎn)變成四條聲明;這說明 CSS 雖然提倡屬性合并,但是最終還是會(huì)進(jìn)行拆分的;所以屬性合并的作用應(yīng)該在于減少 CSS 的代碼量。
計(jì)算
為什么需要計(jì)算?
因?yàn)橐粋€(gè)節(jié)點(diǎn)可能會(huì)有多個(gè) Selector 命中它,這就需要把所有匹配的 Rule 組合起來,再設(shè)置最后的樣式。
準(zhǔn)備工作
為了便于計(jì)算,在生成 CSSStyleSheet 對(duì)象后,會(huì)把 CSSStyleSheet 對(duì)象最右邊 Selector 類型相同的 Rules 存放到對(duì)應(yīng)的 Hash Map 中,比如說所有最右邊 Selector 類型是 id 的 Rules 就會(huì)存放到 ID Rule Map 中;使用最右邊 Selector 的原因是為了更快的匹配當(dāng)前元素的所有 Rule,然后每條 Rule 再檢查自己的下一個(gè) Selector 是否匹配當(dāng)前元素。
idRules
classRules
tagRules
...
*
選擇器命中
一個(gè)節(jié)點(diǎn)想要獲取到所有匹配的 Rule,需要依次判斷 Hash Map 中的 Selector 類型(id、class、tagName 等)是否匹配當(dāng)前節(jié)點(diǎn),如果匹配就會(huì)篩選當(dāng)前 Selector 類型的所有 Rule,找到符合的 Rule 就會(huì)放入結(jié)果集合中;需要注意的是通配符總會(huì)在最后進(jìn)行篩選。
從右向左匹配規(guī)則
上文說過 Hash Map 存放的是最右邊 Selector 類型的 Rule,所以在查找符合的 Rule 最開始,檢驗(yàn)的是當(dāng)前 Rule 最右邊的 Selector;如果這一步通過,下面就要判斷當(dāng)前的 Selector 是不是最左邊的 Selector;如果是,匹配成功,放入結(jié)果集合;否則,說明左邊還有 Selector,遞歸檢查左邊的 Selector 是否匹配,如果不匹配,繼續(xù)檢查下一個(gè) Rule。
為什么需要從右向左匹配呢?
先思考一下正向匹配是什么流程,我們用 div p .yellow 來舉例,先查找所有 div 節(jié)點(diǎn),再向下查找后代是否是 p 節(jié)點(diǎn),如果是,再向下查找是否存在包含 class="yellow" 的節(jié)點(diǎn),如果存在則匹配;但是不存在呢?就浪費(fèi)一次查詢,如果一個(gè)頁面有上千個(gè) div 節(jié)點(diǎn),而只有一個(gè)節(jié)點(diǎn)符合 Rule,就會(huì)造成大量無效查詢,并且如果大多數(shù)無效查詢都在最后發(fā)現(xiàn),那損失的性能就實(shí)在太大了。
這時(shí)再思考從右向左匹配的好處,如果一個(gè)節(jié)點(diǎn)想要找到匹配的 Rule,會(huì)先查詢最右邊 Selector 是當(dāng)前節(jié)點(diǎn)的 Rule,再向左依次檢驗(yàn) Selector;在這種匹配規(guī)則下,開始就能避免大多無效的查詢,當(dāng)然性能就更好,速度更快了。
設(shè)置樣式
設(shè)置樣式的順序是先繼承父節(jié)點(diǎn),然后使用用戶代理的樣式,最后使用開發(fā)者(authorStyleSheet)的樣式。
authorStyleSheet 優(yōu)先級(jí)
放入結(jié)果集合的同時(shí)會(huì)計(jì)算這條 Rule 的優(yōu)先級(jí);來看看 blink 內(nèi)核對(duì)優(yōu)先級(jí)權(quán)重的定義:
switch (m_match) {
case Id:
return 0x010000;
case PseudoClass:
return 0x000100;
case Class:
case PseudoElement:
case AttributeExact:
case AttributeSet:
case AttributeList:
case AttributeHyphen:
case AttributeContain:
case AttributeBegin:
case AttributeEnd:
return 0x000100;
case Tag:
return 0x000001;
case Unknown:
return 0;
}
return 0;
因?yàn)榻馕?Rule 的順序是從右向左進(jìn)行的,所以計(jì)算優(yōu)先級(jí)也會(huì)按照這個(gè)順序取得對(duì)應(yīng) Selector 的權(quán)重后相加。來看幾個(gè)例子:
/*
* 65793 = 65536 + 1 + 256
*/
#container p .text {
font-size: 16px;
}
/*
* 2 = 1 + 1
*/
div p {
font-size: 14px;
}
當(dāng)前節(jié)點(diǎn)所有匹配的 Rule 都放入結(jié)果集合之后,先根據(jù)優(yōu)先級(jí)從小到大排序,如果有優(yōu)先級(jí)相同的 Rule,則比較它們的位置。
內(nèi)聯(lián)樣式優(yōu)先級(jí)
authorStyleSheet 的 Rule 處理完畢,才會(huì)設(shè)置內(nèi)聯(lián)樣式;內(nèi)聯(lián)樣式在構(gòu)建 DOM Tree 的時(shí)候就已經(jīng)處理完成并存放到節(jié)點(diǎn)的 style 屬性上了。
內(nèi)聯(lián)樣式會(huì)放到已經(jīng)排序的結(jié)果集合最后,所以如果不設(shè)置 !important,內(nèi)聯(lián)樣式的優(yōu)先級(jí)是最大的。
!important 優(yōu)先級(jí)
在設(shè)置 !important 的聲明前,會(huì)先設(shè)置不包含 !important 的所有聲明,之后再添加到結(jié)果集合的尾部;因?yàn)檫@個(gè)集合是按照優(yōu)先級(jí)從小到大排序好的,所以 !important 的優(yōu)先級(jí)就變成最大的了。
書寫 CSS 的規(guī)則
結(jié)果集合最后會(huì)生成 ComputedStyle 對(duì)象,可以通過 window.getComputedStyle 方法來查看所有聲明。


可以發(fā)現(xiàn)圖中的聲明是沒有順序的,說明書寫規(guī)則的最大作用是為了良好的閱讀體驗(yàn),利于團(tuán)隊(duì)協(xié)作。
調(diào)整 Style
這一步會(huì)調(diào)整相關(guān)的聲明;例如聲明了 position: absolute;,當(dāng)前節(jié)點(diǎn)的 display 就會(huì)設(shè)置成 block。
參考資料
- 從Chrome源碼看瀏覽器如何計(jì)算CSS
- 探究 CSS 解析原理
- Webkit內(nèi)核探究【2】——Webkit CSS實(shí)現(xiàn)
- Webkit CSS引擎分析
- css加載會(huì)造成阻塞嗎?
- 原來 CSS 與 JS 是這樣阻塞 DOM 解析和渲染的
- 外鏈 CSS 延遲 DOM 解析和 DOMContentLoaded
- CSS/JS 阻塞 DOM 解析和渲染
- 構(gòu)建對(duì)象模型 —— CSS 對(duì)象模型 (CSSOM)
- 阻塞渲染的 CSS
Render Object Tree
在 DOM Tree 和 CSSOM Tree 構(gòu)建完畢之后,才會(huì)開始生成 Render Object Tree(Document 節(jié)點(diǎn)是特例)。
創(chuàng)建 Render Object
在創(chuàng)建 Document 節(jié)點(diǎn)的時(shí)候,會(huì)同時(shí)創(chuàng)建一個(gè) Render Object 作為樹根。Render Object 是一個(gè)描述節(jié)點(diǎn)位置、大小等樣式的可視化對(duì)象。
每個(gè)非 display: none | contents 的節(jié)點(diǎn)都會(huì)創(chuàng)建一個(gè) Render Object,流程大致如下:生成 ComputedStyle(在 CSSOM Tree 計(jì)算這一節(jié)中有講),之后比較新舊 ComputedStyle(開始時(shí)舊的 ComputedStyle 默認(rèn)是空);不同則創(chuàng)建一個(gè)新的 Render Object,并與當(dāng)前處理的節(jié)點(diǎn)關(guān)聯(lián),再建立父子兄弟關(guān)系,從而形成一棵完整的 Render Object Tree。
布局(重排)
Render Object 在添加到樹之后,還需要重新計(jì)算位置和大小;ComputedStyle 里面已經(jīng)包含了這些信息,為什么還需要重新計(jì)算呢?因?yàn)橄?margin: 0 auto; 這樣的聲明是不能直接使用的,需要轉(zhuǎn)化成實(shí)際的大小,才能通過繪圖引擎繪制節(jié)點(diǎn);這也是 DOM Tree 和 CSSOM Tree 需要組合成 Render Object Tree 的原因之一。
布局是從 Root Render Object 開始遞歸的,每一個(gè) Render Object 都有對(duì)自身進(jìn)行布局的方法。為什么需要遞歸(也就是先計(jì)算子節(jié)點(diǎn)再回頭計(jì)算父節(jié)點(diǎn))計(jì)算位置和大小呢?因?yàn)橛行┎季中畔⑿枰庸?jié)點(diǎn)先計(jì)算,之后才能通過子節(jié)點(diǎn)的布局信息計(jì)算出父節(jié)點(diǎn)的位置和大小;例如父節(jié)點(diǎn)的高度需要子節(jié)點(diǎn)撐起。如果子節(jié)點(diǎn)的寬度是父節(jié)點(diǎn)高度的 50%,要怎么辦呢?這就需要在計(jì)算子節(jié)點(diǎn)之前,先計(jì)算自身的布局信息,再傳遞給子節(jié)點(diǎn),子節(jié)點(diǎn)根據(jù)這些信息計(jì)算好之后就會(huì)告訴父節(jié)點(diǎn)是否需要重新計(jì)算。
數(shù)值類型
所有相對(duì)的測量值(rem、em、百分比...)都必須轉(zhuǎn)換成屏幕上的絕對(duì)像素。如果是 em 或 rem,則需要根據(jù)父節(jié)點(diǎn)或根節(jié)點(diǎn)計(jì)算出像素。如果是百分比,則需要乘以父節(jié)點(diǎn)寬或高的最大值。如果是 auto,需要用 (父節(jié)點(diǎn)的寬或高 - 當(dāng)前節(jié)點(diǎn)的寬或高) / 2 計(jì)算出兩側(cè)的值。
盒模型
眾所周知,文檔的每個(gè)元素都被表示為一個(gè)矩形的盒子(盒模型),通過它可以清晰的描述 Render Object 的布局結(jié)構(gòu);在 blink 的源碼注釋中,已經(jīng)生動(dòng)的描述了盒模型,與原先耳熟能詳?shù)牟煌瑵L動(dòng)條也包含在了盒模型中,但是滾動(dòng)條的大小并不是所有的瀏覽器都能修改的。
// ***** THE BOX MODEL *****
// The CSS box model is based on a series of nested boxes:
// http://www.w3.org/TR/CSS21/box.html
// top
// |----------------------------------------------------|
// | |
// | margin-top |
// | |
// | |-----------------------------------------| |
// | | | |
// | | border-top | |
// | | | |
// | | |--------------------------|----| | |
// | | | | | | |
// | | | padding-top |####| | |
// | | | |####| | |
// | | | |----------------| |####| | |
// | | | | | | | | |
// left | ML | BL | PL | content box | PR | SW | BR | MR |
// | | | | | | | | |
// | | | |----------------| | | | |
// | | | | | | |
// | | | padding-bottom | | | |
// | | |--------------------------|----| | |
// | | | ####| | | |
// | | | scrollbar height ####| SC | | |
// | | | ####| | | |
// | | |-------------------------------| | |
// | | | |
// | | border-bottom | |
// | | | |
// | |-----------------------------------------| |
// | |
// | margin-bottom |
// | |
// |----------------------------------------------------|
//
// BL = border-left
// BR = border-right
// ML = margin-left
// MR = margin-right
// PL = padding-left
// PR = padding-right
// SC = scroll corner (contains UI for resizing (see the 'resize' property)
// SW = scrollbar width
box-sizing
box-sizing: content-box | border-box,content-box 遵循標(biāo)準(zhǔn)的 W3C 盒子模型,border-box 遵守 IE 盒子模型。
它們的區(qū)別在于 content-box 只包含 content area,而 border-box 則一直包含到 border。通過一個(gè)例子說明:
// width
// content-box: 40
// border-box: 40 + (2 * 2) + (1 * 2)
div {
width: 40px;
height: 40px;
padding: 2px;
border: 1px solid #ccc;
}
參考資料
- 從Chrome源碼看瀏覽器如何layout布局
- Chromium網(wǎng)頁Render Object Tree創(chuàng)建過程分析
- 瀏覽器的工作原理:新式網(wǎng)絡(luò)瀏覽器幕后揭秘 —— 呈現(xiàn)樹和 DOM 樹的關(guān)系
- 談?wù)勎覍?duì)盒模型的理解
- 渲染樹構(gòu)建、布局及繪制
Render Layer Tree
Render Layer 是在 Render Object 創(chuàng)建的同時(shí)生成的,具有相同坐標(biāo)空間的 Render Object 屬于同一個(gè) Render Layer。這棵樹主要用來實(shí)現(xiàn)層疊上下文,以保證用正確的順序合成頁面。
創(chuàng)建 Render Layer
滿足層疊上下文條件的 Render Object 一定會(huì)為其創(chuàng)建新的 Render Layer,不過一些特殊的 Render Object 也會(huì)創(chuàng)建一個(gè)新的 Render Layer。
創(chuàng)建 Render Layer 的原因如下:
- NormalLayer
- position 屬性為 relative、fixed、sticky、absolute
- 透明的(opacity 小于 1)、濾鏡(filter)、遮罩(mask)、混合模式(mix-blend-mode 不為 normal)
- 剪切路徑(clip-path)
- 2D 或 3D 轉(zhuǎn)換(transform 不為 none)
- 隱藏背面(backface-visibility: hidden)
- 倒影(box-reflect)
- column-count(不為 auto)或者column-widthZ(不為 auto)
- 對(duì)不透明度(opacity)、變換(transform)、濾鏡(filter)應(yīng)用動(dòng)畫
- OverflowClipLayer
- 剪切溢出內(nèi)容(overflow: hidden)
另外以下 DOM 元素對(duì)應(yīng)的 Render Object 也會(huì)創(chuàng)建單獨(dú)的 Render Layer:
- Document
- HTML
- Canvas
- Video
如果是 NoLayer 類型,那它并不會(huì)創(chuàng)建 Render Layer,而是與其第一個(gè)擁有 Render Layer 的父節(jié)點(diǎn)共用一個(gè)。
參考資料
- 無線性能優(yōu)化:Composite —— 從 LayoutObjects 到 PaintLayers
- Chromium網(wǎng)頁Render Layer Tree創(chuàng)建過程分析
- WEBKIT 渲染不可不知的這四棵樹
Graphics Layer Tree
軟件渲染
軟件渲染是瀏覽器最早采用的渲染方式。在這種方式中,渲染是從后向前(遞歸)繪制 Render Layer 的;在繪制一個(gè) Render Layer 的過程中,它的 Render Objects 不斷向一個(gè)共享的 Graphics Context 發(fā)送繪制請(qǐng)求來將自己繪制到一張共享的位圖中。
硬件渲染
有些特殊的 Render Layer 會(huì)繪制到自己的后端存儲(chǔ)(當(dāng)前 Render Layer 會(huì)有自己的位圖),而不是整個(gè)網(wǎng)頁共享的位圖中,這些 Layer 被稱為 Composited Layer(Graphics Layer)。最后,當(dāng)所有的 Composited Layer 都繪制完成之后,會(huì)將它們合成到一張最終的位圖中,這一過程被稱為 Compositing;這意味著如果網(wǎng)頁某個(gè) Render Layer 成為 Composited Layer,那整個(gè)網(wǎng)頁只能通過合成來渲染。除此之外,Compositing 還包括 transform、scale、opacity 等操作,所以這就是硬件加速性能好的原因,上面的動(dòng)畫操作不需要重繪,只需要重新合成就好。
上文提到軟件渲染只會(huì)有一個(gè) Graphics Context,并且所有的 Render Layer 都會(huì)使用同一個(gè) Graphics Context 繪制。而硬件渲染需要多張位圖合成才能得到一張完整的圖像,這就需要引入 Graphics Layer Tree。
Graphics Layer Tree 是根據(jù) Render Layer Tree 創(chuàng)建的,但并不是每一個(gè) Render Layer 都會(huì)有對(duì)應(yīng)的 Composited Layer;這是因?yàn)閯?chuàng)建大量的 Composited Layer 會(huì)消耗非常多的系統(tǒng)內(nèi)存,所以 Render Layer 想要成為 Composited Layer,必須要給出創(chuàng)建的理由,這些理由實(shí)際上就是在描述 Render Layer 具備的特征。如果一個(gè) Render Layer 不是 Compositing Layer,那就和它的祖先共用一個(gè)。
每一個(gè) Graphics Layer 都會(huì)有對(duì)應(yīng)的 Graphics Context。Graphics Context 負(fù)責(zé)輸出當(dāng)前 Render Layer 的位圖,位圖存儲(chǔ)在系統(tǒng)內(nèi)存中,作為紋理(可以理解為 GPU 中的位圖)上傳到 GPU 中,最后 GPU 將多張位圖合成,然后繪制到屏幕上。因?yàn)?Graphics Layer 會(huì)有單獨(dú)的位圖,所以在一般情況下更新網(wǎng)頁的時(shí)候硬件渲染不像軟件渲染那樣重新繪制相關(guān)的 Render Layer;而是重新繪制發(fā)生更新的 Graphics Layer。
提升原因
Render Layer 提升為 Composited Layer 的理由大致概括如下,更為詳細(xì)的說明可以查看 無線性能優(yōu)化:Composite —— 從 PaintLayers 到 GraphicsLayers。
- iframe 元素具有 Composited Layer。
- video 元素及它的控制欄。
- 使用 WebGL 的 canvas 元素。
- 硬件加速插件,例如 flash。
- 3D 或透視變換(perspective transform) CSS 屬性。
- backface-visibility 為 hidden。
- 對(duì) opacity、transform、fliter、backdropfilter 應(yīng)用了 animation 或者 transition(需要是 active 的 animation 或者 transition,當(dāng) animation 或者 transition 效果未開始或結(jié)束后,提升的 Composited Layer 會(huì)恢復(fù)成普通圖層)。
- will-change 設(shè)置為 opacity、transform、top、left、bottom、right(其中 top、left 等需要設(shè)置明確的定位屬性,如 relative 等)。
- 有 Composited Layer 后代并本身具有某些屬性。
- 元素有一個(gè) z-index 較低且為 Composited Layer 的兄弟元素。
為什么需要 Composited Layer?
- 避免不必要的重繪。例如網(wǎng)頁中有兩個(gè) Layer a 和 b,如果 a Layer 的元素發(fā)生改變,b Layer 沒有發(fā)生改變;那只需要重新繪制 a Layer,然后再與 b Layer 進(jìn)行 Compositing,就可以得到整個(gè)網(wǎng)頁。
- 利用硬件加速高效實(shí)現(xiàn)某些 UI 特性。例如滾動(dòng)、3D 變換、透明度或者濾鏡效果,可以通過 GPU(硬件渲染)高效實(shí)現(xiàn)。
層壓縮
由于重疊的原因,可能會(huì)產(chǎn)生大量的 Composited Layer,就會(huì)浪費(fèi)很多資源,嚴(yán)重影響性能,這個(gè)問題被稱為層爆炸。瀏覽器通過 Layer Squashing(層壓縮)處理這個(gè)問題,當(dāng)有多個(gè) Render Layer 與 Composited Layer 重疊,這些 Render Layer 會(huì)被壓縮到同一個(gè) Composited Layer。來看一個(gè)例子:
Document


可以看到后面兩個(gè)節(jié)點(diǎn)重疊而壓縮到了同一個(gè) Composited Layer。
有一些不能被壓縮的情況,可以在 無線性能優(yōu)化:Composite —— 層壓縮 中查看。
參考資料
- 無線性能優(yōu)化:Composite —— 從-PaintLayers-到-GraphicsLayers
- Webkit 渲染基礎(chǔ)與硬件加速
- Chromium網(wǎng)頁Graphics Layer Tree創(chuàng)建過程分析
- Chrome中的硬件加速合成
- 瀏覽器渲染流程 詳細(xì)分析
- WebKit 渲染流程基礎(chǔ)及分層加速
相關(guān)文章
- 從 10 多秒到 1.05 秒!前端性能優(yōu)化實(shí)踐
- JavaScript 啟動(dòng)性能瓶頸分析與解決方案
- 從前端性能優(yōu)化引申出來的5道經(jīng)典面試題(值得收藏)

2. ECMAScript 重溫系列(10篇全)
3. JavaScript設(shè)計(jì)模式 重溫系列(9篇全)4.?正則 / 框架 / 算法等 重溫系列(16篇全)5.?Webpack4 入門(上)||?Webpack4 入門(下)6.?MobX 入門(上)?||??MobX 入門(下)7. 80+篇原創(chuàng)系列匯總
回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~
點(diǎn)擊“閱讀原文”查看 80+ 篇原創(chuàng)文章
