瀏覽器如何渲染網(wǎng)頁
要了解瀏覽器渲染頁面的過程,首先得知道一個名詞——關(guān)鍵渲染路徑。關(guān)鍵渲染路徑是指瀏覽器從最初接收請求來的HTML、CSS、javascript等資源,然后解析、構(gòu)建樹、渲染布局、繪制,最后呈現(xiàn)給用戶能看到的界面這整個過程。
用戶看到頁面實際上可以分為兩個階段:頁面內(nèi)容加載完成和頁面資源加載完成,分別對應(yīng)于DOMContentLoaded和Load。
-
DOMContentLoaded事件觸發(fā)時,僅當DOM加載完成,不包括樣式表,圖片等 -
load事件觸發(fā)時,頁面上所有的DOM,樣式表,腳本,圖片都已加載完成
瀏覽器渲染的過程主要包括以下五步:
- 瀏覽器將獲取的HTML文檔解析成DOM樹。
- 處理CSS標記,構(gòu)成層疊樣式表模型CSSOM(CSS Object Model)。
- 將DOM和CSSOM合并為渲染樹(
rendering tree),代表一系列將被渲染的對象。 - 渲染樹的每個元素包含的內(nèi)容都是計算過的,它被稱之為布局
layout。瀏覽器使用一種流式處理的方法,只需要一次繪制操作就可以布局所有的元素。 - 將渲染樹的各個節(jié)點繪制到屏幕上,這一步被稱為繪制
painting。
需要注意的是,以上五個步驟并不一定一次性順序完成,比如DOM或CSSOM被修改時,亦或是哪個過程會重復(fù)執(zhí)行,這樣才能計算出哪些像素需要在屏幕上進行重新渲染。而在實際情況中,JavaScript和CSS的某些操作往往會多次修改DOM或者CSSOM。

瀏覽器渲染網(wǎng)頁的具體流程
構(gòu)建DOM樹
當瀏覽器接收到服務(wù)器響應(yīng)來的HTML文檔后,會遍歷文檔節(jié)點,生成DOM樹。
需要注意以下幾點:
- DOM樹在構(gòu)建的過程中可能會被CSS和JS的加載而執(zhí)行阻塞
-
display:none的元素也會在DOM樹中 - 注釋也會在DOM樹中
-
script標簽會在DOM樹中
無論是DOM還是CSSOM,都是要經(jīng)過Bytes→characters→tokens→nodes→object model這個過程。
當前節(jié)點的所有子節(jié)點都構(gòu)建好后才會去構(gòu)建當前節(jié)點的下一個兄弟節(jié)點。
構(gòu)建CSSOM規(guī)則樹
瀏覽器解析CSS文件并生成CSSOM,每個CSS文件都被分析成一個StyleSheet對象,每個對象都包含CSS規(guī)則。CSS規(guī)則對象包含對應(yīng)于CSS語法的選擇器和聲明對象以及其他對象。
在這個過程需要注意的是:
- CSS解析可以與DOM解析同時進行。
- CSS解析與
script的執(zhí)行互斥 。 - 在Webkit內(nèi)核中進行了
script執(zhí)行優(yōu)化,只有在JS訪問CSS時才會發(fā)生互斥。
構(gòu)建渲染樹(Render Tree)
通過DOM樹和CSS規(guī)則樹,瀏覽器就可以通過它兩構(gòu)建渲染樹了。瀏覽器會先從DOM樹的根節(jié)點開始遍歷每個可見節(jié)點,然后對每個可見節(jié)點找到適配的CSS樣式規(guī)則并應(yīng)用。
有以下幾點需要注意:
- Render Tree和DOM Tree不完全對應(yīng)
-
display: none的元素不在Render Tree中 -
visibility: hidden的元素在Render Tree中

渲染樹生成后,還是沒有辦法渲染到屏幕上,渲染到屏幕需要得到各個節(jié)點的位置信息,這就需要布局(Layout)的處理了。
渲染樹布局(layout of the render tree)
布局階段會從渲染樹的根節(jié)點開始遍歷,由于渲染樹的每個節(jié)點都是一個Render Object對象,包含寬高,位置,背景色等樣式信息。所以瀏覽器就可以通過這些樣式信息來確定每個節(jié)點對象在頁面上的確切大小和位置,布局階段的輸出就是我們常說的盒子模型,它會精確地捕獲每個元素在屏幕內(nèi)的確切位置與大小。需要注意的是:
-
float元素,absoulte元素,fixed元素會發(fā)生位置偏移。 - 我們常說的脫離文檔流,其實就是脫離Render Tree。
渲染樹繪制(Painting the render tree)
在繪制階段,瀏覽器會遍歷渲染樹,調(diào)用渲染器的paint()方法在屏幕上顯示其內(nèi)容。渲染樹的繪制工作是由瀏覽器的UI后端組件完成的。
瀏覽器渲染網(wǎng)頁的那些事兒
瀏覽器主要組件結(jié)構(gòu)

渲染引擎主要有兩個:webkit和Gecko
Firefox使用Geoko,Mozilla自主研發(fā)的渲染引擎。Safari和Chrome都使用webkit。Webkit是一款開源渲染引擎,它本來是為linux平臺研發(fā)的,后來由Apple移植到Mac及Windows上。
雖然主流瀏覽器渲染過程叫法有區(qū)別,但是主要流程還是相同的。
渲染阻塞
JS可以操作DOM來修改DOM結(jié)構(gòu),可以操作CSSOM來修改節(jié)點樣式,這就導(dǎo)致了瀏覽器在遇到<script>標簽時,DOM構(gòu)建將暫停,直至腳本完成執(zhí)行,然后繼續(xù)構(gòu)建DOM。如果腳本是外部的,會等待腳本下載完畢,再繼續(xù)解析文檔?,F(xiàn)在可以在script標簽上增加屬性defer或者async。腳本解析會將腳本中改變DOM和CSS的地方分別解析出來,追加到DOM樹和CSSOM規(guī)則樹上。
每次去執(zhí)行JavaScript腳本都會嚴重地阻塞DOM樹的構(gòu)建,如果JavaScript腳本還操作了CSSOM,而正好這個CSSOM還沒有下載和構(gòu)建,瀏覽器甚至?xí)舆t腳本執(zhí)行和構(gòu)建DOM,直至完成其CSSOM的下載和構(gòu)建。所以,script標簽的位置很重要。
JS阻塞了構(gòu)建DOM樹,也阻塞了其后的構(gòu)建CSSOM規(guī)則樹,整個解析進程必須等待JS的執(zhí)行完成才能夠繼續(xù),這就是所謂的JS阻塞頁面。
由于CSSOM負責存儲渲染信息,瀏覽器就必須保證在合成渲染樹之前,CSSOM是完備的,這種完備是指所有的CSS(內(nèi)聯(lián)、內(nèi)部和外部)都已經(jīng)下載完,并解析完,只有CSSOM和DOM的解析完全結(jié)束,瀏覽器才會進入下一步的渲染,這就是CSS阻塞渲染。
CSS阻塞渲染意味著,在CSSOM完備前,頁面將一直處理白屏狀態(tài),這就是為什么樣式放在head中,僅僅是為了更快的解析CSS,保證更快的首次渲染。
需要注意的是,即便你沒有給頁面任何的樣式聲明,CSSOM依然會生成,默認生成的CSSOM自帶瀏覽器默認樣式。
當解析HTML的時候,會把新來的元素插入DOM樹里面,同時去查找CSS,然后把對應(yīng)的樣式規(guī)則應(yīng)用到元素上,查找樣式表是按照從右到左的順序去匹配的。
例如:div p {font-size: 16px},會先尋找所有p標簽并判斷它的父標簽是否為div之后才會決定要不要采用這個樣式進行渲染)。
所以,我們平時寫CSS時,盡量用id和class,千萬不要過渡層疊。
回流和重繪(reflow和repaint)
我們都知道HTML默認是流式布局的,但CSS和JS會打破這種布局,改變DOM的外觀樣式以及大小和位置。因此我們就需要知道兩個概念:replaint和reflow。
reflow(回流)
當瀏覽器發(fā)現(xiàn)布局發(fā)生了變化,這個時候就需要倒回去重新渲染,這個回退的過程叫reflow。reflow會從html這個root frame開始遞歸往下,依次計算所有的結(jié)點幾何尺寸和位置,以確認是渲染樹的一部分發(fā)生變化還是整個渲染樹。reflow幾乎是無法避免的,因為只要用戶進行交互操作,就勢必會發(fā)生頁面的一部分的重新渲染,且通常我們也無法預(yù)估瀏覽器到底會reflow哪一部分的代碼,因為他們會相互影響。
repaint(重繪)
repaint則是當我們改變某個元素的背景色、文字顏色、邊框顏色等等不影響它周圍或內(nèi)部布局的屬性時,屏幕的一部分要重畫,但是元素的幾何尺寸和位置沒有發(fā)生改變。
需要注意的是,display:none會觸發(fā)reflow,而visibility: hidden屬性則并不算是不可見屬性,它的語義是隱藏元素,但元素仍然占據(jù)著布局空間,它會被渲染成一個空框。所以visibility:hidden只會觸發(fā)repaint,因為沒有發(fā)生位置變化。
另外有些情況下,比如修改了元素的樣式,瀏覽器并不會立刻reflow或repaint一次,而是會把這樣的操作積攢一批,然后做一次reflow,這又叫異步reflow或增量異步reflow。但是在有些情況下,比如resize窗口,改變了頁面默認的字體等。對于這些操作,瀏覽器會馬上進行reflow。
引起reflow
現(xiàn)代瀏覽器會對回流做優(yōu)化,它會等到足夠數(shù)量的變化發(fā)生,再做一次批處理回流。
- 頁面第一次渲染(初始化)
- DOM樹變化(如:增刪節(jié)點)
- Render樹變化(如:
padding改變) - 瀏覽器窗口
resize - 獲取元素的某些屬性
瀏覽器為了獲得正確的值也會提前觸發(fā)回流,這樣就使得瀏覽器的優(yōu)化失效了,這些屬性包括offsetLeft、offsetTop、offsetWidth、offsetHeight、 scrollTop/Left/Width/Height、clientTop/Left/Width/Height、調(diào)用了getComputedStyle()。
引起repaint
reflow回流必定引起repaint重繪,重繪可以單獨觸發(fā)。
背景色、顏色、字體改變(注意:字體大小發(fā)生變化時,會觸發(fā)回流)
減少reflow、repaint觸發(fā)次數(shù)
- 用
transform做形變和位移可以減少reflow - 避免逐個修改節(jié)點樣式,盡量一次性修改
- 使用
DocumentFragment將需要多次修改的DOM元素緩存,最后一次性append到真實DOM中渲染 - 可以將需要多次修改的DOM元素設(shè)置
display:none,操作完再顯示。(因為隱藏元素不在render樹內(nèi),因此修改隱藏元素不會觸發(fā)回流重繪) - 避免多次讀取某些屬性
- 通過絕對位移將復(fù)雜的節(jié)點元素脫離文檔流,形成新的Render Layer,降低回流成本
幾條關(guān)于優(yōu)化渲染效率的建議
結(jié)合上文有以下幾點可以優(yōu)化渲染效率。
- 合法地去書寫HTML和CSS ,且不要忘了文檔編碼類型。
- 樣式文件應(yīng)當在
head標簽中,而腳本文件在body結(jié)束前,這樣可以防止阻塞的方式。 - 簡化并優(yōu)化CSS選擇器,盡量將嵌套層減少到最小。
- DOM 的多個讀操作(或多個寫操作),應(yīng)該放在一起。不要兩個讀操作之間,加入一個寫操作。
- 如果某個樣式是通過重排得到的,那么最好緩存結(jié)果。避免下一次用到的時候,瀏覽器又要重排。
- 不要一條條地改變樣式,而要通過改變
class,或者csstext屬性,一次性地改變樣式。 - 盡量用
transform來做形變和位移 - 盡量使用離線DOM,而不是真實的網(wǎng)頁DOM,來改變元素樣式。比如,操作
Document Fragment對象,完成后再把這個對象加入DOM。再比如,使用cloneNode()方法,在克隆的節(jié)點上進行操作,然后再用克隆的節(jié)點替換原始節(jié)點。 - 先將元素設(shè)為
display: none(需要1次重排和重繪),然后對這個節(jié)點進行100次操作,最后再恢復(fù)顯示(需要1次重排和重繪)。這樣一來,你就用兩次重新渲染,取代了可能高達100次的重新渲染。 -
position屬性為absolute或fixed的元素,重排的開銷會比較小,因為不用考慮它對其他元素的影響。 - 只在必要的時候,才將元素的
display屬性為可見,因為不可見的元素不影響重排和重繪。另外,visibility : hidden的元素只對重繪有影響,不影響重排。 - 使用
window.requestAnimationFrame()、window.requestIdleCallback()這兩個方法調(diào)節(jié)重新渲染。