瀏覽器如何渲染網(wǎng)頁(yè)
要了解瀏覽器渲染頁(yè)面的過(guò)程,首先得知道一個(gè)名詞——關(guān)鍵渲染路徑烘跺。關(guān)鍵渲染路徑是指瀏覽器從最初接收請(qǐng)求來(lái)的HTML萎坷、CSS、javascript等資源成艘,然后解析拇砰、構(gòu)建樹(shù)、渲染布局狰腌、繪制除破,最后呈現(xiàn)給用戶能看到的界面這整個(gè)過(guò)程。
用戶看到頁(yè)面實(shí)際上可以分為兩個(gè)階段:頁(yè)面內(nèi)容加載完成和頁(yè)面資源加載完成琼腔,分別對(duì)應(yīng)于DOMContentLoaded
和Load
瑰枫。
-
DOMContentLoaded
事件觸發(fā)時(shí),僅當(dāng)DOM加載完成,不包括樣式表光坝,圖片等 -
load
事件觸發(fā)時(shí)尸诽,頁(yè)面上所有的DOM,樣式表盯另,腳本性含,圖片都已加載完成
瀏覽器渲染的過(guò)程主要包括以下五步:
- 瀏覽器將獲取的HTML文檔解析成DOM樹(shù)。
- 處理CSS標(biāo)記鸳惯,構(gòu)成層疊樣式表模型CSSOM(CSS Object Model)商蕴。
- 將DOM和CSSOM合并為渲染樹(shù)(
rendering tree
),代表一系列將被渲染的對(duì)象芝发。 - 渲染樹(shù)的每個(gè)元素包含的內(nèi)容都是計(jì)算過(guò)的绪商,它被稱之為布局
layout
。瀏覽器使用一種流式處理的方法辅鲸,只需要一次繪制操作就可以布局所有的元素格郁。 - 將渲染樹(shù)的各個(gè)節(jié)點(diǎn)繪制到屏幕上,這一步被稱為繪制
painting
独悴。
需要注意的是例书,以上五個(gè)步驟并不一定一次性順序完成,比如DOM或CSSOM被修改時(shí)刻炒,亦或是哪個(gè)過(guò)程會(huì)重復(fù)執(zhí)行决采,這樣才能計(jì)算出哪些像素需要在屏幕上進(jìn)行重新渲染。而在實(shí)際情況中落蝙,JavaScript和CSS的某些操作往往會(huì)多次修改DOM或者CSSOM织狐。
瀏覽器渲染網(wǎng)頁(yè)的具體流程
構(gòu)建DOM樹(shù)
當(dāng)瀏覽器接收到服務(wù)器響應(yīng)來(lái)的HTML文檔后暂幼,會(huì)遍歷文檔節(jié)點(diǎn)筏勒,生成DOM樹(shù)。
需要注意以下幾點(diǎn):
- DOM樹(shù)在構(gòu)建的過(guò)程中可能會(huì)被CSS和JS的加載而執(zhí)行阻塞
-
display:none
的元素也會(huì)在DOM樹(shù)中 - 注釋也會(huì)在DOM樹(shù)中
-
script
標(biāo)簽會(huì)在DOM樹(shù)中
無(wú)論是DOM還是CSSOM旺嬉,都是要經(jīng)過(guò)Bytes→characters→tokens→nodes→object model
這個(gè)過(guò)程管行。
當(dāng)前節(jié)點(diǎn)的所有子節(jié)點(diǎn)都構(gòu)建好后才會(huì)去構(gòu)建當(dāng)前節(jié)點(diǎn)的下一個(gè)兄弟節(jié)點(diǎn)。
構(gòu)建CSSOM規(guī)則樹(shù)
瀏覽器解析CSS文件并生成CSSOM邪媳,每個(gè)CSS文件都被分析成一個(gè)StyleSheet對(duì)象捐顷,每個(gè)對(duì)象都包含CSS規(guī)則。CSS規(guī)則對(duì)象包含對(duì)應(yīng)于CSS語(yǔ)法的選擇器和聲明對(duì)象以及其他對(duì)象雨效。
在這個(gè)過(guò)程需要注意的是:
- CSS解析可以與DOM解析同時(shí)進(jìn)行迅涮。
- CSS解析與
script
的執(zhí)行互斥 。 - 在Webkit內(nèi)核中進(jìn)行了
script
執(zhí)行優(yōu)化徽龟,只有在JS訪問(wèn)CSS時(shí)才會(huì)發(fā)生互斥叮姑。
構(gòu)建渲染樹(shù)(Render Tree)
通過(guò)DOM樹(shù)和CSS規(guī)則樹(shù),瀏覽器就可以通過(guò)它兩構(gòu)建渲染樹(shù)了。瀏覽器會(huì)先從DOM樹(shù)的根節(jié)點(diǎn)開(kāi)始遍歷每個(gè)可見(jiàn)節(jié)點(diǎn)传透,然后對(duì)每個(gè)可見(jiàn)節(jié)點(diǎn)找到適配的CSS樣式規(guī)則并應(yīng)用耘沼。
有以下幾點(diǎn)需要注意:
- Render Tree和DOM Tree不完全對(duì)應(yīng)
-
display: none
的元素不在Render Tree中 -
visibility: hidden
的元素在Render Tree中
渲染樹(shù)生成后,還是沒(méi)有辦法渲染到屏幕上朱盐,渲染到屏幕需要得到各個(gè)節(jié)點(diǎn)的位置信息群嗤,這就需要布局(Layout)的處理了。
渲染樹(shù)布局(layout of the render tree)
布局階段會(huì)從渲染樹(shù)的根節(jié)點(diǎn)開(kāi)始遍歷兵琳,由于渲染樹(shù)的每個(gè)節(jié)點(diǎn)都是一個(gè)Render Object對(duì)象狂秘,包含寬高,位置闰围,背景色等樣式信息赃绊。所以瀏覽器就可以通過(guò)這些樣式信息來(lái)確定每個(gè)節(jié)點(diǎn)對(duì)象在頁(yè)面上的確切大小和位置,布局階段的輸出就是我們常說(shuō)的盒子模型羡榴,它會(huì)精確地捕獲每個(gè)元素在屏幕內(nèi)的確切位置與大小碧查。需要注意的是:
-
float
元素,absoulte
元素校仑,fixed
元素會(huì)發(fā)生位置偏移忠售。 - 我們常說(shuō)的脫離文檔流,其實(shí)就是脫離Render Tree迄沫。
渲染樹(shù)繪制(Painting the render tree)
在繪制階段稻扬,瀏覽器會(huì)遍歷渲染樹(shù),調(diào)用渲染器的paint()
方法在屏幕上顯示其內(nèi)容羊瘩。渲染樹(shù)的繪制工作是由瀏覽器的UI后端組件完成的泰佳。
瀏覽器渲染網(wǎng)頁(yè)的那些事兒
瀏覽器主要組件結(jié)構(gòu)
渲染引擎主要有兩個(gè):webkit和Gecko
Firefox使用Geoko,Mozilla自主研發(fā)的渲染引擎尘吗。Safari和Chrome都使用webkit逝她。Webkit是一款開(kāi)源渲染引擎,它本來(lái)是為linux平臺(tái)研發(fā)的睬捶,后來(lái)由Apple移植到Mac及Windows上黔宛。
雖然主流瀏覽器渲染過(guò)程叫法有區(qū)別,但是主要流程還是相同的擒贸。
渲染阻塞
JS可以操作DOM來(lái)修改DOM結(jié)構(gòu)臀晃,可以操作CSSOM來(lái)修改節(jié)點(diǎn)樣式,這就導(dǎo)致了瀏覽器在遇到<script>
標(biāo)簽時(shí)介劫,DOM構(gòu)建將暫停徽惋,直至腳本完成執(zhí)行,然后繼續(xù)構(gòu)建DOM座韵。如果腳本是外部的险绘,會(huì)等待腳本下載完畢,再繼續(xù)解析文檔。現(xiàn)在可以在script
標(biāo)簽上增加屬性defer
或者async
隆圆。腳本解析會(huì)將腳本中改變DOM和CSS的地方分別解析出來(lái)漱挚,追加到DOM樹(shù)和CSSOM規(guī)則樹(shù)上。
每次去執(zhí)行JavaScript腳本都會(huì)嚴(yán)重地阻塞DOM樹(shù)的構(gòu)建渺氧,如果JavaScript腳本還操作了CSSOM旨涝,而正好這個(gè)CSSOM還沒(méi)有下載和構(gòu)建,瀏覽器甚至?xí)舆t腳本執(zhí)行和構(gòu)建DOM侣背,直至完成其CSSOM的下載和構(gòu)建白华。所以,script
標(biāo)簽的位置很重要贩耐。
JS阻塞了構(gòu)建DOM樹(shù)弧腥,也阻塞了其后的構(gòu)建CSSOM規(guī)則樹(shù),整個(gè)解析進(jìn)程必須等待JS的執(zhí)行完成才能夠繼續(xù)潮太,這就是所謂的JS阻塞頁(yè)面管搪。
由于CSSOM負(fù)責(zé)存儲(chǔ)渲染信息,瀏覽器就必須保證在合成渲染樹(shù)之前铡买,CSSOM是完備的更鲁,這種完備是指所有的CSS(內(nèi)聯(lián)、內(nèi)部和外部)都已經(jīng)下載完奇钞,并解析完澡为,只有CSSOM和DOM的解析完全結(jié)束,瀏覽器才會(huì)進(jìn)入下一步的渲染景埃,這就是CSS阻塞渲染媒至。
CSS阻塞渲染意味著,在CSSOM完備前谷徙,頁(yè)面將一直處理白屏狀態(tài)拒啰,這就是為什么樣式放在head
中,僅僅是為了更快的解析CSS蒂胞,保證更快的首次渲染图呢。
需要注意的是条篷,即便你沒(méi)有給頁(yè)面任何的樣式聲明骗随,CSSOM依然會(huì)生成,默認(rèn)生成的CSSOM自帶瀏覽器默認(rèn)樣式赴叹。
當(dāng)解析HTML的時(shí)候鸿染,會(huì)把新來(lái)的元素插入DOM樹(shù)里面,同時(shí)去查找CSS乞巧,然后把對(duì)應(yīng)的樣式規(guī)則應(yīng)用到元素上涨椒,查找樣式表是按照從右到左的順序去匹配的。
例如:div p {font-size: 16px}
,會(huì)先尋找所有p
標(biāo)簽并判斷它的父標(biāo)簽是否為div
之后才會(huì)決定要不要采用這個(gè)樣式進(jìn)行渲染)蚕冬。
所以免猾,我們平時(shí)寫CSS時(shí),盡量用id
和class
囤热,千萬(wàn)不要過(guò)渡層疊猎提。
回流和重繪(reflow和repaint)
我們都知道HTML默認(rèn)是流式布局的,但CSS和JS會(huì)打破這種布局旁蔼,改變DOM的外觀樣式以及大小和位置锨苏。因此我們就需要知道兩個(gè)概念:replaint
和reflow
。
reflow(回流)
當(dāng)瀏覽器發(fā)現(xiàn)布局發(fā)生了變化棺聊,這個(gè)時(shí)候就需要倒回去重新渲染伞租,這個(gè)回退的過(guò)程叫reflow
。reflow
會(huì)從html
這個(gè)root frame
開(kāi)始遞歸往下限佩,依次計(jì)算所有的結(jié)點(diǎn)幾何尺寸和位置葵诈,以確認(rèn)是渲染樹(shù)的一部分發(fā)生變化還是整個(gè)渲染樹(shù)。reflow
幾乎是無(wú)法避免的祟同,因?yàn)橹灰脩暨M(jìn)行交互操作驯击,就勢(shì)必會(huì)發(fā)生頁(yè)面的一部分的重新渲染,且通常我們也無(wú)法預(yù)估瀏覽器到底會(huì)reflow
哪一部分的代碼耐亏,因?yàn)樗麄儠?huì)相互影響徊都。
repaint(重繪)
repaint
則是當(dāng)我們改變某個(gè)元素的背景色、文字顏色广辰、邊框顏色等等不影響它周圍或內(nèi)部布局的屬性時(shí)暇矫,屏幕的一部分要重畫,但是元素的幾何尺寸和位置沒(méi)有發(fā)生改變择吊。
需要注意的是李根,display:none
會(huì)觸發(fā)reflow
,而visibility: hidden
屬性則并不算是不可見(jiàn)屬性几睛,它的語(yǔ)義是隱藏元素房轿,但元素仍然占據(jù)著布局空間,它會(huì)被渲染成一個(gè)空框所森。所以visibility:hidden
只會(huì)觸發(fā)repaint
囱持,因?yàn)闆](méi)有發(fā)生位置變化。
另外有些情況下焕济,比如修改了元素的樣式纷妆,瀏覽器并不會(huì)立刻reflow
或repaint
一次,而是會(huì)把這樣的操作積攢一批晴弃,然后做一次reflow
掩幢,這又叫異步reflow
或增量異步reflow
逊拍。但是在有些情況下,比如resize
窗口际邻,改變了頁(yè)面默認(rèn)的字體等芯丧。對(duì)于這些操作,瀏覽器會(huì)馬上進(jìn)行reflow
世曾。
引起reflow
現(xiàn)代瀏覽器會(huì)對(duì)回流做優(yōu)化注整,它會(huì)等到足夠數(shù)量的變化發(fā)生,再做一次批處理回流度硝。
- 頁(yè)面第一次渲染(初始化)
- DOM樹(shù)變化(如:增刪節(jié)點(diǎn))
- Render樹(shù)變化(如:
padding
改變) - 瀏覽器窗口
resize
- 獲取元素的某些屬性
瀏覽器為了獲得正確的值也會(huì)提前觸發(fā)回流肿轨,這樣就使得瀏覽器的優(yōu)化失效了,這些屬性包括offsetLeft蕊程、offsetTop椒袍、offsetWidth、offsetHeight藻茂、 scrollTop/Left/Width/Height驹暑、clientTop/Left/Width/Height
、調(diào)用了getComputedStyle()
辨赐。
引起repaint
reflow
回流必定引起repaint
重繪优俘,重繪可以單獨(dú)觸發(fā)。
背景色掀序、顏色帆焕、字體改變(注意:字體大小發(fā)生變化時(shí),會(huì)觸發(fā)回流)
減少reflow不恭、repaint觸發(fā)次數(shù)
- 用
transform
做形變和位移可以減少reflow
- 避免逐個(gè)修改節(jié)點(diǎn)樣式叶雹,盡量一次性修改
- 使用
DocumentFragment
將需要多次修改的DOM元素緩存,最后一次性append
到真實(shí)DOM中渲染 - 可以將需要多次修改的DOM元素設(shè)置
display:none
换吧,操作完再顯示折晦。(因?yàn)殡[藏元素不在render
樹(shù)內(nèi),因此修改隱藏元素不會(huì)觸發(fā)回流重繪) - 避免多次讀取某些屬性
- 通過(guò)絕對(duì)位移將復(fù)雜的節(jié)點(diǎn)元素脫離文檔流沾瓦,形成新的Render Layer满着,降低回流成本
幾條關(guān)于優(yōu)化渲染效率的建議
結(jié)合上文有以下幾點(diǎn)可以優(yōu)化渲染效率。
- 合法地去書寫HTML和CSS 贯莺,且不要忘了文檔編碼類型风喇。
- 樣式文件應(yīng)當(dāng)在
head
標(biāo)簽中,而腳本文件在body
結(jié)束前乖篷,這樣可以防止阻塞的方式响驴。 - 簡(jiǎn)化并優(yōu)化CSS選擇器透且,盡量將嵌套層減少到最小撕蔼。
- DOM 的多個(gè)讀操作(或多個(gè)寫操作)豁鲤,應(yīng)該放在一起。不要兩個(gè)讀操作之間鲸沮,加入一個(gè)寫操作琳骡。
- 如果某個(gè)樣式是通過(guò)重排得到的,那么最好緩存結(jié)果讼溺。避免下一次用到的時(shí)候楣号,瀏覽器又要重排。
- 不要一條條地改變樣式怒坯,而要通過(guò)改變
class
炫狱,或者csstext
屬性,一次性地改變樣式剔猿。 - 盡量用
transform
來(lái)做形變和位移 - 盡量使用離線DOM视译,而不是真實(shí)的網(wǎng)頁(yè)DOM,來(lái)改變?cè)貥邮焦榫础1热缈岷僮?code>Document Fragment對(duì)象,完成后再把這個(gè)對(duì)象加入DOM汪茧。再比如椅亚,使用
cloneNode()
方法,在克隆的節(jié)點(diǎn)上進(jìn)行操作舱污,然后再用克隆的節(jié)點(diǎn)替換原始節(jié)點(diǎn)呀舔。 - 先將元素設(shè)為
display: none
(需要1次重排和重繪),然后對(duì)這個(gè)節(jié)點(diǎn)進(jìn)行100次操作扩灯,最后再恢復(fù)顯示(需要1次重排和重繪)别威。這樣一來(lái),你就用兩次重新渲染驴剔,取代了可能高達(dá)100次的重新渲染省古。 -
position
屬性為absolute
或fixed
的元素,重排的開(kāi)銷會(huì)比較小丧失,因?yàn)椴挥每紤]它對(duì)其他元素的影響豺妓。 - 只在必要的時(shí)候,才將元素的
display
屬性為可見(jiàn)布讹,因?yàn)椴豢梢?jiàn)的元素不影響重排和重繪琳拭。另外,visibility : hidden
的元素只對(duì)重繪有影響描验,不影響重排白嘁。 - 使用
window.requestAnimationFrame()
、window.requestIdleCallback()
這兩個(gè)方法調(diào)節(jié)重新渲染膘流。