一、瀏覽器如何渲染網(wǎng)頁
要了解瀏覽器渲染頁面的過程针炉,首先得知道一個名詞——關(guān)鍵路徑渲染。關(guān)鍵渲染路徑(Critical Rendering Path)是指與當(dāng)前用戶操作有關(guān)的內(nèi)容扳抽。例如用戶在瀏覽器中打開一個頁面篡帕,其中頁面所顯示的東西就是當(dāng)前用戶操作相關(guān)的內(nèi)容,也就是瀏覽器從服務(wù)器那收到的HTML,CSS,JavaScript等相關(guān)資源贸呢,然后經(jīng)過一系列處理后渲染出來web頁面镰烧。實(shí)際抽象出來理解可以將這些步驟看作一個函數(shù),就輸入HTML楞陷,經(jīng)過一層層的處理怔鳖,最后輸出像素。
而瀏覽器渲染的過程主要包括以下幾步:
- 瀏覽器將獲取的HTML文檔并解析成DOM樹固蛾。
- 將 css 文件處理成 StyleSheet 對象结执,從而進(jìn)行樣式計算。
- 根據(jù)dom樹和StyleSheet 生成布局樹艾凯。
- 根據(jù)具體的節(jié)點(diǎn)信息對頁面進(jìn)行分層處理献幔,生成圖層樹
- 根據(jù)圖層樹生成繪制列表
- 合成線程通過主線程提交的繪制列表對圖層進(jìn)行分塊,并進(jìn)行柵格化趾诗,生成位圖
- 合成位圖蜡感,并將其顯示
具體如下圖過程如下圖所示:
需要注意的是,以上幾個步驟并不一定是一次性順序完成恃泪,比如 DOM 被修改時郑兴,亦或是哪個過程會重復(fù)執(zhí)行,這樣才能計算出哪些像素需要在屏幕上進(jìn)行重新渲染悟泵。而在實(shí)際情況中杈笔,JavaScript和CSS的某些操作往往會多次修改DOM或者CSSOM。
值得注意的的是糕非,在每個階段蒙具,都會有對應(yīng)的輸入,處理朽肥,以及輸出禁筏。下面我們就來詳細(xì)的了解一下這幾個過程及需要注意的事項(xiàng)。
二衡招、瀏覽器渲染網(wǎng)頁的具體流程
2.1 構(gòu)建DOM樹
因?yàn)闉g覽器無法直接使用HTML/SVG/XHTML篱昔,因此當(dāng)瀏覽器客戶端從服務(wù)器那接受到HTML文檔后,就會遍歷文檔節(jié)點(diǎn)始腾,然后對這些文檔節(jié)點(diǎn)通過HTML解析器進(jìn)行解析州刽,最后生成DOM樹,所生成的 DOM 樹結(jié)構(gòu)和HTML標(biāo)簽一一對應(yīng)浪箭。需要注意的是穗椅,在這其中HTML解析器會進(jìn)行諸如:標(biāo)記化算法,樹構(gòu)建算法等操作奶栖,其中的規(guī)范即遵循了W3C的相應(yīng)規(guī)范匹表,也都有瀏覽器引擎自己的一些特定的操作,詳情可以翻閱這篇非常著名的文章:
在此階段宣鄙,輸入的即是一個HTML文件袍镀,然后會有瀏覽器的HTML解析器對其進(jìn)行解析,輸出樹形結(jié)構(gòu)的DOM樹冻晤。值得注意的是苇羡,HTML解析器并不是等整個文檔全部加載完之后才開始解析的,而是網(wǎng)絡(luò)進(jìn)程加載了多少數(shù)據(jù)鼻弧,HTML解析器就會解析多少數(shù)據(jù)设江。相當(dāng)與在網(wǎng)絡(luò)進(jìn)程與渲染進(jìn)程之間會在這期間建立一個數(shù)據(jù)共享的管道,網(wǎng)絡(luò)進(jìn)程每次收到數(shù)據(jù)都會將其轉(zhuǎn)發(fā)到渲染進(jìn)程温数,從而保證渲染進(jìn)程中的HTML解析器可以源源不斷的獲取到用于渲染的數(shù)據(jù)绣硝。這個過程可以理解為下方這個過程:
[圖片上傳失敗...(image-79abab-1574600053183)]
- 將字節(jié)流通過分詞器轉(zhuǎn)化為 Token
- 根據(jù) Token 生成節(jié)點(diǎn) node
- 根據(jù)生成的節(jié)點(diǎn),組成 DOM 樹
每個頁面的DOM樹撑刺,我們也可以直接通過在控制臺輸入document 來進(jìn)行訪問:
對于DOM樹鹉胖,我們需要注意以下幾點(diǎn):
- DOM 樹從內(nèi)容上來看和 HTML 幾乎一模一樣,但 DOM 是保存在內(nèi)存中的樹形結(jié)構(gòu)够傍,可以通過 JavaScript 來查詢和修改甫菠。
<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">document.getElementsByTagName("h2")[0].innerText = "Hello World"</pre>
- display:none 的元素也會在 DOM 樹中。
- 注釋也會在 DOM 樹中
- script 標(biāo)簽會在 DOM 樹中
- DOM 樹在構(gòu)建的過程中可能會被 CSS 和 JS 的加載而執(zhí)行阻塞冕屯。
此外DOM 樹在構(gòu)建的過程中可能會被 CSS 和 JS 的加載而執(zhí)行阻塞寂诱,也就是我們常說的阻塞渲染。這是因?yàn)镠TML文件是通過HTML解析器轉(zhuǎn)化成 DOM 樹的安聘,而在HTML解析器中如果遇到了 JavaScript 腳本痰洒,HTML 解析器會先執(zhí)行 JavaScript 腳本瓢棒,待這個腳本執(zhí)行完成之后,再繼續(xù)往下解析丘喻。因此我們常說脯宿,將script標(biāo)簽放在body下面,通常就是基于這種考慮的泉粉。但為什么CSS也有可能會阻塞DOM樹的構(gòu)建呢连霉,可以看下面一個栗子:
<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);"><html> <head> <style type="text/css" src = "demo.css" /> </head> <body> <p>demo</p> <script> const p = document.getElementsByTagName('p')[0] p.innerText = 'hello world' p.style.color = 'red' </script> </body> </html></pre>
由于任何script代碼都能改變HTML的結(jié)構(gòu),因此HTML每次遇到script都會停止解析嗡靡,等待JavaScript腳本被執(zhí)行完成之后跺撼,再進(jìn)行接下來的解析,而當(dāng)我們通過 JavaScript 去進(jìn)行樣式操作的時候讨彼,這個 JavaScript 腳本執(zhí)行完成的前提條件就成了需要現(xiàn)將樣式信息確定下來歉井。因此在這種情況下,HTML解析器能否繼續(xù)執(zhí)行下去点骑,以及繼續(xù)執(zhí)行的時間酣难,也需要取決與這個CSS文件給不給面子了。這也是我們常說的黑滴,別在 JavaScript 中操作樣式的原因憨募。
為了優(yōu)化這種情況,現(xiàn)代瀏覽器也做了一些優(yōu)化袁辈,比如預(yù)解析操作菜谣。當(dāng)渲染引擎接收到字節(jié)流后,會開啟一個預(yù)解析線程晚缩,用來分析 HTML文件的代碼中的JS尾膊,CSS文件,解析到相關(guān)文件的時候荞彼,預(yù)解析進(jìn)行會提前下載這些資源冈敛。
對于處理這種事情,避免阻塞的產(chǎn)生鸣皂,我們也有以下幾點(diǎn)可以注意的:
- 在引入順序上抓谴,CSS 資源先于 JavaScript 資源。
- JavaScript 應(yīng)盡量少的去影響 DOM 的構(gòu)建寞缝。
- 可以將 JavaScript 腳本設(shè)置為異步加載癌压,通過 async 或 defer 來標(biāo)記代碼
2.2 計算樣式
在構(gòu)建渲染樹時,需要計算每一個呈現(xiàn)對象的可視化的屬性值荆陆。而這個過程就被稱為樣式計算或者計算樣式滩届。這個過程主要是為了 DOM 樹中每個節(jié)點(diǎn)的具體樣式,大致可分為三大步驟:
- 將 CSS 解析為瀏覽器能理解的 StyleSheet
- 轉(zhuǎn)換樣式表中的屬性值被啼,使其標(biāo)準(zhǔn)化
- 計算出 DOM 樹中每個節(jié)點(diǎn)的具體樣式
2.2.1 將 CSS 解析為瀏覽器能理解的 styleSheet
和html一個道理帜消,瀏覽器也無法直接去理解我們所寫的那些CSS樣式棠枉,因此瀏覽器在接收到CSS文件后,會將CSS文件轉(zhuǎn)換為瀏覽器所能理解的 StyleSheet券犁。轉(zhuǎn)化了的 StyleSheet 我們同樣也可以通過控制臺來訪問:
在這個過程中需要注意的是:
- CSS解析可以與DOM解析同時進(jìn)行术健。
- CSS解析與 script 的執(zhí)行互斥 汹碱。
- 在Webkit內(nèi)核中進(jìn)行了script執(zhí)行優(yōu)化粘衬,只有在JS訪問CSS時才會發(fā)生互斥。
- CSS樣式不管是來自于 link 的外部引用咳促,還是style標(biāo)記內(nèi)的CSS稚新,亦或是元素的style屬性內(nèi)嵌的CSS,都會被解析成styleSheets跪腹。
2.2.2 轉(zhuǎn)換樣式表中的屬性值褂删,使其標(biāo)準(zhǔn)化
在將CSS文轉(zhuǎn)化為瀏覽器能夠理解的 styleSheet 后,就需要對期進(jìn)行進(jìn)行屬性值的標(biāo)準(zhǔn)化操作了冲茸。這里的標(biāo)準(zhǔn)化的意思就是屯阀,我們在寫css文件的時候,會寫一些語義化的屬性比如:red/bold等等轴术。但其實(shí)這些詞對于渲染引擎來說难衰,卻不是那么好理解的。因此在進(jìn)行計算樣式之前逗栽,瀏覽器還會這對這些不怎么好計算的值進(jìn)行標(biāo)準(zhǔn)化盖袭,將其轉(zhuǎn)化為渲染引擎容易理解的詞,比如將red轉(zhuǎn)化成為 rgb(255, 0, 0)等等彼宠。
2.2.3 計算出 DOM 樹中每個節(jié)點(diǎn)的具體樣式
計算出 DOM 樹中每個節(jié)點(diǎn)的具體樣式主要涉及的就是CSS繼承規(guī)則和層疊規(guī)則了鳄虱,對于繼承規(guī)則其實(shí)比較好理解,就是凭峡,每個DOM節(jié)點(diǎn)都包含的父節(jié)點(diǎn)的樣式拙已。
而層疊規(guī)則也就是樣式層疊就有點(diǎn)麻煩了,MDN是這么描述層疊的:
層疊是CSS的一個基本特征摧冀,它是一個定義了如何合并來自多個源的屬性值的算法倍踪。它在CSS處于核心地位,CSS的全稱層疊樣式表正是強(qiáng)調(diào)了這一點(diǎn)按价。
層疊的具體細(xì)節(jié)在這里也不展開講了(我自己現(xiàn)在還沒搞清楚惭适。。楼镐。)癞志,大家可以去CSS層疊看看其內(nèi)部的一些規(guī)則。
在有了css繼承規(guī)則和層疊規(guī)則后框产,樣式計算的這個階段就會在這兩個規(guī)則的基礎(chǔ)上對 DOM 節(jié)點(diǎn)中的每個元素計算處具體的樣式凄杯,這個階段中最終輸出的結(jié)果會保存在 ComputedStyle 中错洁,這個同樣可以通過控制臺進(jìn)行查看:
2.3 布局階段
通過前面兩個階段,我們已經(jīng)得到了DOM樹以及DOM樹中具體每個元素的樣式了戒突,但對于每個元素所處的幾何位置我們現(xiàn)在還是不知道的屯碴,因此接下來要做的就是計算出DOM樹中可見元素的幾何位置。這個過程可以分為兩個階段:
- 創(chuàng)建布局樹
- 布局計算
2.3.1 創(chuàng)建布局樹
由于DOM樹還包含很多不可見的元素膊存,比如head標(biāo)簽导而,script標(biāo)簽,以及設(shè)置為display:none的屬性隔崎,因?yàn)闉g覽器勢必不能將所有的dom樹的元素都全部拿來進(jìn)行布局計算今艺,因此在這個階段,瀏覽器會額外構(gòu)建一顆只包含可見元素的布局樹爵卒。在構(gòu)建布局樹期間虚缎,瀏覽器大體會進(jìn)行以下一些工作:
- 遍歷DOM樹中的所有可見節(jié)點(diǎn),并將這些節(jié)點(diǎn)加到布局中钓株。
- 將所有不可見節(jié)點(diǎn)忽略掉
下面兩個需要注意:
- display: none的元素不在Render Tree中
- visibility: hidden的元素在Render Tree中
2.3.2 布局計算
在已經(jīng)獲取了所有可見元素的樹之后实牡,就可以計算布局樹節(jié)點(diǎn)的幾何位置了。HTML是基于流的布局方式轴合,因此大多數(shù)情況下创坞,只需要進(jìn)行一次遍歷即刻計算出頁面的幾何信息。通常來說值桩,處于流靠后的元素不會影響到靠前位置元素的幾何特征摆霉,因此在進(jìn)行布局計算的時候,通常是按從左至右奔坟,從上至下的順序遍歷文檔(只是通常而言携栋,比如表格啥的就不是這樣)。
布局計算是一個遞歸的過程咳秉,它從根節(jié)點(diǎn)出發(fā)婉支,然后遞歸遍歷部分或所有的節(jié)點(diǎn),為每一個需要計算的呈現(xiàn)器計算幾何信息澜建。這個計算量無疑是龐大的向挖,因此為了避免一些較小的更改也會觸發(fā)頁面的整體布局計算,瀏覽器將布局方式分為了全局布局和增量布局炕舵。
- 全局布局:全局布局是指觸發(fā)了整個布局樹的布局計算的布局何之,包括:屏幕大小改動,字體大小改動等
- 增量布局:增量布局是指當(dāng)某個呈現(xiàn)器發(fā)生改變了咽筋,只對相應(yīng)的呈現(xiàn)器進(jìn)行布局計算溶推。
在執(zhí)行完布局計算后,會將布局計算的結(jié)果寫入布局樹中,因此這個過程可以理解為一種裝飾者模式蒜危,輸入輸出都是一個布局樹虱痕,只是在這個過程中會將布局計算的結(jié)果給加進(jìn)去。
2.4 分層
在有了布局樹之后辐赞,瀏覽器的還是不能直接根據(jù)布局樹來將頁面給畫出來部翘,因?yàn)轫撁嬷羞€存在中一些特殊的效果,比如頁面滾動响委,z-index等新思。為了能夠方便的實(shí)現(xiàn)這些花里胡哨的功能,渲染引擎還需要進(jìn)行一個分層處理晃酒,將特定節(jié)點(diǎn)生成轉(zhuǎn)筒的圖層表牢,并生成一個圖層樹(LayerTree),這個我們也能通過瀏覽器的面板看到:
如上圖所示贝次,瀏覽器的頁面實(shí)際上被分成了多個圖層,這些圖層疊加在一起就形成了我們最終所看到的頁面彰导。需要注意的是蛔翅,并不是布局樹中的每一個節(jié)點(diǎn)都會包含一個圖層,因此如果一個節(jié)點(diǎn)沒有所對應(yīng)的圖層位谋,那么它就會從屬于父節(jié)點(diǎn)的圖層山析。如果一個節(jié)點(diǎn)需要有自己的圖層,通常需要滿足以下聯(lián)合條件
- 擁有層疊上下文屬性的元素
- 需要剪裁(clip)
2.5 圖層繪制
在確定好圖層之后掏父,瀏覽器的渲染引擎會對圖層樹中的每個圖層進(jìn)行繪制笋轨,渲染引擎會將一個圖層的繪制拆封成很多個小的繪制指令,然后會將這些繪制指令按照一定順序組成一個待繪制列表赊淑。和布局相同爵政,繪制也分為全局和增量兩種,也是為了避免部分圖層的改變而需要對整個圖層樹進(jìn)行繪制陶缺。此外钾挟,CSS也對繪制順序做了規(guī)定:
- 背景顏色
- 背景圖片
- 邊框
- 子代
- 輪廓
2.6 柵格化(raster)操作
這里的柵格化是指將圖轉(zhuǎn)化為位圖。繪制列表只是用來記錄繪制順序和繪制指令的列表饱岸,而實(shí)際繪制操作是由渲染引擎中的合成線程來完成的掺出。實(shí)際過程是當(dāng)圖層對應(yīng)的繪制列表準(zhǔn)備好之后,主線程會將繪制列表提交給合成線程苫费。 合成線程會根據(jù)用戶所能見的窗口范圍對一些劃分汤锨,將一些大的圖層化分為圖塊。然后合成線程會根據(jù)用戶所見范圍附近的圖塊來優(yōu)先生成位圖百框,實(shí)際生成位圖的操作是由柵格化來執(zhí)行的闲礼。圖塊是柵格化執(zhí)行的最小單元,渲染進(jìn)程維護(hù)了一個柵格化的線程池,所有的圖塊柵格化操作都會在這個線程池里進(jìn)行位仁。
通常柑贞,柵格化會使用GPU進(jìn)程中的GPU來進(jìn)行加速,使用GPU進(jìn)程生成位圖的過程叫快速柵格化聂抢,通過這個方式生成的位圖會被保存在GPU內(nèi)存中钧嘶。這樣做的好處就在于,當(dāng)渲染進(jìn)程的主線程發(fā)生阻塞的時候琳疏,合成線程以及GPU進(jìn)程不會受其影響有决,可以正常運(yùn)行。這也是為啥有時候主線程卡住了空盼,但CSS動畫依然可以風(fēng)騷依舊的原因书幕。
2.7 合成和顯示
在所有的圖塊都被進(jìn)行柵格化后,合成線程就會生成繪制圖塊的命令——“DrawQuad”揽趾,然后將該命令提交給瀏覽器進(jìn)程台汇。瀏覽器進(jìn)程里面有一個叫 viz 的組件,用來接收合成線程發(fā)過來的 DrawQuad 命令篱瞎,然后根據(jù) DrawQuad 命令苟呐,將其頁面內(nèi)容繪制到內(nèi)存中,最后再將內(nèi)存顯示在屏幕上俐筋。
三牵素、瀏覽器渲染網(wǎng)頁的那些事兒
3.1 回流和重繪(reflow和repaint)
我們都知道HTML默認(rèn)是流式布局的,但CSS和JS會打破這種布局澄者,改變DOM的外觀樣式以及大小和位置笆呆。因此我們就需要知道兩個概念:
- reflow(回流):當(dāng)瀏覽器發(fā)現(xiàn)某個部分發(fā)生了變化從而影響了布局,這個時候就需要倒回去重新渲染粱挡,大家稱這個回退的過程叫 reflow赠幕。 常見的reflow是一些會影響頁面布局的操作,諸如Tab抱怔,隱藏等劣坊。reflow 會從 html 這個 root frame 開始遞歸往下,依次計算所有的結(jié)點(diǎn)幾何尺寸和位置屈留,以確認(rèn)是渲染樹的一部分發(fā)生變化還是整個渲染樹局冰。reflow幾乎是無法避免的,因?yàn)橹灰脩暨M(jìn)行交互操作灌危,就勢必會發(fā)生頁面的一部分的重新渲染康二,且通常我們也無法預(yù)估瀏覽器到底會reflow哪一部分的代碼,因?yàn)樗麄儠嗷ビ绊憽?/li>
- repaint(重繪): repaint則是當(dāng)我們改變某個元素的背景色勇蝙、文字顏色沫勿、邊框顏色等等不影響它周圍或內(nèi)部布局的屬性時,屏幕的一部分要重畫,但是元素的幾何尺寸和位置沒有發(fā)生改變产雹。
需要注意的是诫惭,display:none 會觸發(fā) reflow,而visibility: hidden屬性則并不算是不可見屬性蔓挖,它的語義是隱藏元素夕土,但元素仍然占據(jù)著布局空間,它會被渲染成一個空框瘟判,這在我們上面有提到過怨绣。所以visibility:hidden 只會觸發(fā) repaint,因?yàn)闆]有發(fā)生位置變化拷获。
我們不能避免reflow篮撑,但還是能通過一些操作來減少回流:
- 用transform做形變和位移.
- 通過絕對位移來脫離當(dāng)前層疊上下文,形成新的Render Layer匆瓜。
另外有些情況下赢笨,比如修改了元素的樣式,瀏覽器并不會立刻reflow 或 repaint 一次陕壹,而是會把這樣的操作積攢一批质欲,然后做一次 reflow,這又叫異步 reflow 或增量異步 reflow糠馆。但是在有些情況下,比如resize 窗口怎憋,改變了頁面默認(rèn)的字體等又碌。對于這些操作,瀏覽器會馬上進(jìn)行 reflow。
3.2 幾條關(guān)于優(yōu)化渲染效率的建議
結(jié)合上文和我看到的一些文章,有以下幾點(diǎn)可以優(yōu)化渲染效率
- 合法地去書寫 HTML 和 CSS 挚赊,且不要忘了文檔編碼類型住涉。
- 樣式文件應(yīng)當(dāng)在 head 標(biāo)簽中,而腳本文件在 body 結(jié)束前咽斧,這樣可以防止阻塞的方式。
- 簡化并優(yōu)化CSS選擇器,盡量將嵌套層減少到最小躁垛。
- 盡量減少在 JavaScript 中進(jìn)行DOM操作。
- 修改元素樣式時圾笨,更改其class屬性是性能最高的方法教馆。
- 盡量用 transform 來做形變和位移
參考資料:
https://www.html5rocks.com/en/tutorials/internals/howbrowserswork/