渲染引擎及關(guān)鍵渲染路徑(Critical Rendering Path)
通過(guò)網(wǎng)絡(luò)模塊加載到HTML文件后渲染引擎渲染流程如下,這也通常被稱作關(guān)鍵渲染路徑(Critical Rendering Path)
- 構(gòu)建DOM樹(shù)(DOM tree):從上到下解析HTML文檔生成DOM節(jié)點(diǎn)樹(shù)(DOM tree)遂填,也叫內(nèi)容樹(shù)(content tree)
- 構(gòu)建CSSOM(CSS Object Model)樹(shù):加載解析樣式生成CSSOM樹(shù)
- 執(zhí)行JavaScript:加載并執(zhí)行JavaScript代碼(包括內(nèi)聯(lián)代碼或外聯(lián)JavaScript文件)
- 構(gòu)建渲染樹(shù)(render tree):根據(jù)DOM樹(shù)和CSSOM樹(shù),生成渲染樹(shù)(render tree)
- 布局(layout):根據(jù)渲染樹(shù)將節(jié)點(diǎn)樹(shù)的每一個(gè)節(jié)點(diǎn)布局在屏幕上的正確位置
- 繪制(painting):遍歷渲染樹(shù)繪制所有節(jié)點(diǎn),為每一個(gè)節(jié)點(diǎn)適用對(duì)應(yīng)的樣式欧宜,這一過(guò)程是通過(guò)UI后端模塊完成
為了更友好的用戶體驗(yàn)茬祷,瀏覽器會(huì)盡可能快的展現(xiàn)內(nèi)容清焕,而不會(huì)等到文檔所有內(nèi)容到達(dá)才開(kāi)始解析和構(gòu)建/布局渲染樹(shù),而是每次處理一部分牲迫,并展現(xiàn)在屏幕上耐朴,這也是為什么我們經(jīng)辰栉裕可以看到頁(yè)面加載的時(shí)候內(nèi)容是從上到下一點(diǎn)一點(diǎn)展現(xiàn)的盹憎。
渲染引擎流程
Webkit渲染引擎流程如下圖:
Gecko渲染引擎流程如下圖:
如上圖,Webkit瀏覽器和Gecko瀏覽器渲染流程大致相同,不同的是:
- Webkit瀏覽器中的渲染樹(shù)(render tree)铐刘,在Gecko瀏覽器中對(duì)應(yīng)的則是框架樹(shù)(frame tree),渲染對(duì)象(render object)對(duì)應(yīng)的是框架(frame);
- Webkit中的布局(Layout)過(guò)程陪每,在Gecko中稱為回流(Reflow),本質(zhì)是一樣的,后文會(huì)解釋回流的另一層含義–重新布局镰吵;
- Gecko中HTML和DOM樹(shù)中間多了一層內(nèi)容池(Content sink),可以理解成生成DOM元素的工廠檩禾。
單線程
渲染引擎是單線程工作的,意味著渲染流程是一步一步漸進(jìn)完成的疤祭。
解析文檔(PARSER HTML)
在詳細(xì)介紹瀏覽器渲染文檔之前盼产,先應(yīng)該理解瀏覽器如何解析文檔:解析文檔的順序,對(duì)于CSS和JavaScript如何處理等勺馆。
解析順序
瀏覽器按從上到下的順序掃描解析文檔戏售;-
解析樣式和腳本
- 腳本
由于通常會(huì)在JavaScript腳本中改變文檔DOM結(jié)構(gòu)侨核,于是瀏覽器以同步方式解析,加載和執(zhí)行腳本灌灾,瀏覽器在解析文檔時(shí)搓译,當(dāng)解析到<script>標(biāo)簽時(shí),會(huì)解析其中的腳本(對(duì)于外鏈的JavaScript文件锋喜,需要先加載該文件內(nèi)容些己,再進(jìn)行解析),然后立即執(zhí)行嘿般,這整個(gè)過(guò)程都會(huì)阻塞文檔解析段标,直到腳本執(zhí)行完才會(huì)繼續(xù)解析文檔。就是說(shuō)由于腳本是同步加載和執(zhí)行的博个,它會(huì)阻塞文檔解析怀樟,這也解釋了為什么現(xiàn)在通常建議將
<script>
標(biāo)簽放在</body>
標(biāo)簽前面,而不是放在<head>
標(biāo)簽里∨栌叮現(xiàn)在HTML5提供defer和async兩個(gè)屬性支持延遲和異步加載JavaScript文件往堡,如:
- 腳本
<script defer src="script.js">
- 改進(jìn)
針對(duì)上文說(shuō)的腳本阻塞文檔解析,主流瀏覽器如Chrome和FireFox等都有一些優(yōu)化共耍,比如在執(zhí)行腳本時(shí)虑灰,開(kāi)啟另一個(gè)線程解析剩余的文檔以找出并加載其他的待下載外部資源(不改變主線程的DOM樹(shù),僅優(yōu)化加載外部資源)痹兜。
- 樣式
不同于腳本世吨,瀏覽器對(duì)樣式的處理并不會(huì)阻塞文檔解析,大概是因?yàn)闃邮奖聿⒉粫?huì)改變DOM結(jié)構(gòu)辙喂。
- 樣式表與腳本
你可能想問(wèn)樣式是否會(huì)阻塞腳本文件的加載執(zhí)行呢莺奔?正常情況是不會(huì)的,但是存在一個(gè)問(wèn)題是通常我們會(huì)在腳本中請(qǐng)求樣式信息遗淳,但是在文檔解析時(shí)拍柒,如果樣式尚未加載或解析,將會(huì)得到錯(cuò)誤信息屈暗,對(duì)于這一問(wèn)題拆讯,F(xiàn)ireFox瀏覽器和Webkit瀏覽器處理策略不同:
- 當(dāng)存在有樣式文件未被加載和解析時(shí),F(xiàn)ireFox瀏覽器會(huì)阻塞所有腳本养叛;
- 而Webkit瀏覽器只會(huì)阻塞操作了改文件內(nèi)聲明的樣式屬性的腳本种呐。
構(gòu)建DOM樹(shù)
DOM樹(shù),即文檔內(nèi)所有節(jié)點(diǎn)構(gòu)成的一個(gè)樹(shù)形結(jié)構(gòu)弃甥。
假設(shè)瀏覽器獲取返回的如下HTML文檔:
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="./theme.css"></link>
<script src="./config.js"></script>
<title>關(guān)鍵渲染路徑</title>
</head>
<body>
<h1 class="title">關(guān)鍵渲染路徑</h1>
<p>關(guān)鍵渲染路徑介紹</p>
<footer>@copyright2017</footer>
</body>
</html>
首先瀏覽器從上到下依次解析文檔構(gòu)建DOM樹(shù)爽室,如下:
構(gòu)建CSSOM樹(shù)
CSSOM樹(shù),與DOM樹(shù)結(jié)構(gòu)相似淆攻,只是另外為每一個(gè)節(jié)點(diǎn)關(guān)聯(lián)了樣式信息阔墩。
theme.css樣式內(nèi)容如下:
html, body {
width: 100%;
height: 100%;
background-color: #fcfcfc;
}
.title {
font-size: 20px;
}
.footer {
font-size: 12px;
color: #aaa;
}
構(gòu)建CSSOM樹(shù)如圖:
執(zhí)行JAVASCRIPT
上文已經(jīng)闡述了文檔解析時(shí)對(duì)腳本的處理掉缺,我們得知腳本加載,解析和執(zhí)行會(huì)阻塞文檔解析戈擒,而在特殊情況下樣式的加載和解析也會(huì)阻塞腳本眶明,所以現(xiàn)在推薦的實(shí)踐是<script>
標(biāo)簽放在</body>
標(biāo)簽前面。
構(gòu)建渲染樹(shù)(RENDER TREE)
DOM樹(shù)和CSSOM樹(shù)都構(gòu)建完了筐高,接著瀏覽器會(huì)構(gòu)建渲染樹(shù):
渲染樹(shù)搜囱,代表一個(gè)文檔的視覺(jué)展示,瀏覽器通過(guò)它將文檔內(nèi)容繪制在瀏覽器窗口柑土,展示給用戶蜀肘,它由按順序展示在屏幕上的一系列矩形對(duì)象組成,這些矩形對(duì)象都帶有字體稽屏,顏色和尺寸扮宠,位置等視覺(jué)樣式屬性。對(duì)于這些矩對(duì)象狐榔,F(xiàn)ireFox稱之為框架(frame),Webkit瀏覽器稱之為渲染對(duì)象(render object, renderer)坛增,后文統(tǒng)稱為渲染對(duì)象。
這里把渲染樹(shù)節(jié)點(diǎn)稱為矩形對(duì)象薄腻,是因?yàn)槭盏罚恳粋€(gè)渲染對(duì)象都代表著其對(duì)應(yīng)DOM節(jié)點(diǎn)的CSS盒子,該盒子包含了尺寸,位置等幾何信息庵楷,同時(shí)它指向一個(gè)樣式對(duì)象包含其他視覺(jué)樣式信息罢艾。
渲染樹(shù)與DOM樹(shù)
每一個(gè)渲染對(duì)象都對(duì)應(yīng)著DOM節(jié)點(diǎn),但是非視覺(jué)(隱藏尽纽,不占位)DOM元素不會(huì)插入渲染樹(shù)咐蚯,如<head>
元素或聲明display: none;
的元素,渲染對(duì)象與DOM節(jié)點(diǎn)不是簡(jiǎn)單的一對(duì)一的關(guān)系弄贿,一個(gè)DOM可以對(duì)應(yīng)一個(gè)渲染對(duì)象春锋,但一個(gè)DOM元素也可能對(duì)應(yīng)多個(gè)渲染對(duì)象,因?yàn)橛泻芏嘣夭恢拱粋€(gè)CSS盒子挎春,如當(dāng)文本被折行時(shí)看疙,會(huì)產(chǎn)生多個(gè)行盒豆拨,這些行會(huì)生成多個(gè)渲染對(duì)象直奋;又如行內(nèi)元素同時(shí)包含塊元素和行內(nèi)元素,則會(huì)創(chuàng)建一個(gè)匿名塊級(jí)盒包含內(nèi)部行內(nèi)元素施禾,此時(shí)一個(gè)DOM對(duì)應(yīng)多個(gè)矩形對(duì)象(渲染對(duì)象)脚线。
渲染樹(shù)及其對(duì)應(yīng)DOM樹(shù)如圖:
- 圖中渲染樹(shù)viewport即視口,是文檔的初始包含塊弥搞,scroll代表滾動(dòng)區(qū)域
- 渲染樹(shù)并不會(huì)包含顯式或隱式地
display:none;
的標(biāo)簽元素邮绿。
布局過(guò)程
布局是一個(gè)從上到下渠旁,從外到內(nèi)進(jìn)行的遞歸過(guò)程,從根渲染對(duì)象船逮,即對(duì)應(yīng)著HTML文檔根元素<html>顾腊,然后下一級(jí)渲染對(duì)象,如對(duì)應(yīng)著<body>元素挖胃,如此層層遞歸杂靶,依次計(jì)算每一個(gè)渲染對(duì)象的幾何信息(位置和尺寸)。
幾何信息-位置和尺寸酱鸭,即相對(duì)于窗口的坐標(biāo)和尺寸吗垮,如根渲染對(duì)象,其坐標(biāo)為(0凹髓, 0)烁登,尺寸即是視口
尺寸(瀏覽器窗口的可視區(qū)域)。
每一個(gè)渲染對(duì)象的布局流程基本如:
- 計(jì)算此渲染對(duì)象的寬度(width)蔚舀;
- 遍歷此渲染對(duì)象的所有子級(jí)饵沧,依次:
2.1 設(shè)置子級(jí)渲染對(duì)象的坐標(biāo)
2.2 判斷是否需要觸發(fā)子渲染對(duì)象的布局或回流方法,計(jì)算子渲染對(duì)象的高度(height) - 設(shè)置此渲染對(duì)象的高度:根據(jù)子渲染對(duì)象的累積高赌躺,margin和padding的高度設(shè)置其高度捷泞;
- 設(shè)置此渲染對(duì)象臟位值為false。
繪制(PAINTING)
最后是繪制(paint)階段或重繪(repaint)階段寿谴,瀏覽器UI組件將遍歷渲染樹(shù)并調(diào)用渲染對(duì)象的繪制(paint)方法锁右,將內(nèi)容展現(xiàn)在屏幕上,也有可能在之后對(duì)DOM進(jìn)行修改讶泰,需要重新繪制渲染對(duì)象咏瑟,也就是重繪,繪制和重繪的關(guān)系可以參考布局和回流的關(guān)系痪署。
一個(gè)重要的概念reflow和repaint
- Repaint
屏幕的一部分要重畫(huà)码泞,比如某個(gè)CSS的背景色變了。但是元素的幾何尺寸沒(méi)有變狼犯。
- Reflow
意味著元件的幾何尺寸變了余寥,我們需要重新驗(yàn)證并計(jì)算Render Tree。是Render Tree的一部分或全部發(fā)生了變化悯森。這就是Reflow宋舷,或是Layout。(HTML使用的是flow based layout瓢姻,也就是流式布局祝蝠,所以,如果某元件的幾何尺寸發(fā)生了變化,需要重新布局绎狭,也就叫reflow)reflow 會(huì)從<html>這個(gè)root frame開(kāi)始遞歸往下细溅,依次計(jì)算所有的結(jié)點(diǎn)幾何尺寸和位置,在reflow過(guò)程中儡嘶,可能會(huì)增加一些frame喇聊,比如一個(gè)文本字符串必需被包裝起來(lái)。
Reflow的成本比Repaint的成本高得多的多蹦狂。DOM Tree里的每個(gè)結(jié)點(diǎn)都會(huì)有reflow方法承疲,一個(gè)結(jié)點(diǎn)的reflow很有可能導(dǎo)致子結(jié)點(diǎn),甚至父點(diǎn)以及同級(jí)結(jié)點(diǎn)的reflow鸥咖。在一些高性能的電腦上也許還沒(méi)什么燕鸽,但是如果reflow發(fā)生在手機(jī)上,那么這個(gè)過(guò)程是非常痛苦和耗電的啼辣。
所以啊研,下面這些動(dòng)作有很大可能會(huì)是成本比較高的。
- 當(dāng)你增加鸥拧、刪除党远、修改DOM結(jié)點(diǎn)時(shí),會(huì)導(dǎo)致Reflow或Repaint
- 當(dāng)你移動(dòng)DOM的位置富弦,或是搞個(gè)動(dòng)畫(huà)的時(shí)候沟娱。
- 當(dāng)你修改CSS樣式的時(shí)候。
- 當(dāng)你Resize窗口的時(shí)候(移動(dòng)端沒(méi)有這個(gè)問(wèn)題)腕柜,或是滾動(dòng)的時(shí)候济似。
- 當(dāng)你修改網(wǎng)頁(yè)的默認(rèn)字體時(shí)。
注:display:none會(huì)觸發(fā)reflow盏缤,而visibility:hidden只會(huì)觸發(fā)repaint砰蠢,因?yàn)闆](méi)有發(fā)現(xiàn)位置變化。
減少reflow/repaint
- 不要一條一條地修改DOM的樣式唉铜。與其這樣台舱,還不如預(yù)先定義好css的class,然后修改DOM的className潭流。
- 不要把DOM結(jié)點(diǎn)的屬性值放在一個(gè)循環(huán)里當(dāng)成循環(huán)里的變量竞惋。不然這會(huì)導(dǎo)致大量地讀寫(xiě)這個(gè)結(jié)點(diǎn)的屬性。
- 盡可能的修改層級(jí)比較低的DOM灰嫉。當(dāng)然拆宛,改變層級(jí)比較低的DOM有可能會(huì)造成大面積的reflow,但是也可能影響范圍很小熬甫。
- 為動(dòng)畫(huà)的HTML元件使用fixed或absoult的position胰挑,那么修改他們的CSS是不會(huì)reflow的。
- 千萬(wàn)不要使用table布局椿肩。因?yàn)榭赡芎苄〉囊粋€(gè)小改動(dòng)會(huì)造成整個(gè)table的重新布局瞻颂。