瀏覽器解析渲染以及頁面優(yōu)化
瀏覽器對頁面的加載、解析以及渲染是一個(gè)非常復(fù)雜的話題,這里我只對這個(gè)過程做一個(gè)非常概括的總覽,以初步了解這些過程,對于前端編程如何優(yōu)化頁面給予一些原理性的支撐。
為什么要了解這些過程
- 了解瀏覽器的資源加載,可以使我們在引入外部樣式、腳本資源時(shí)進(jìn)行更合理的時(shí)機(jī)選擇
- 了解瀏覽器文檔解析,可以使我們在構(gòu)建DOM結(jié)構(gòu),組織CSS選擇器等,選擇更為合理的寫法
- 了解瀏覽器結(jié)果渲染,讓我們在設(shè)置元素屬性,腳本操作盡量優(yōu)化以減少和避免不必要的重繪和回流
以上三個(gè)過程并非完全獨(dú)立,而是會有交叉,存在一邊加載一邊解析一邊渲染的工作現(xiàn)象。
加載
從用戶輸入網(wǎng)址,這其后到獲得網(wǎng)頁文檔之前也包含了如下眾多步驟:
- DNS解析,會依次從緩存,本地Hosts,本地DNS服務(wù)器直至根DNS服務(wù)器,任何一級***結(jié)果則直接返回,此步驟獲得目標(biāo)服務(wù)器IP
- 端口解析,從URL中獲得目標(biāo)服務(wù)器端口號
- 瀏覽器建立一條與網(wǎng)頁服務(wù)器的TCP連接(三次握手)
- 瀏覽器向服務(wù)器發(fā)送一條HTTP請求報(bào)文
- 服務(wù)器執(zhí)行一些邏輯如數(shù)據(jù)庫查詢并最終生成并返回HTTP響應(yīng)報(bào)文
初步經(jīng)過以上步驟,瀏覽器獲得一份HTML文檔,然后瀏覽器會對此文檔自上而下進(jìn)行加載,加載過程中就同時(shí)進(jìn)行解析和渲染。
加載過程中,遇到外部的CSS資源,瀏覽器會通過額外的資源線程發(fā)出請求,獲取CSS文件,同樣如果遇到圖片資源,瀏覽器會發(fā)出另一個(gè)請求,獲取圖片資源,這些請求都是異步的,并不影響HTML文檔繼續(xù)加載。但是如果遇到了JS資源,HTML文檔會暫停解析,要等待JS資源加載并解析執(zhí)行完畢,才會重新恢復(fù)HTML文檔解析。
瀏覽器這樣設(shè)計(jì)的考量是JS很有可能修改DOM結(jié)構(gòu),這意味著JS腳本后的一些資源加載、文檔解析可能是不必要的工作。這里就衍生出常見的一條優(yōu)化法則: 將腳本文件加載置于文檔末尾'body'閉合標(biāo)簽之前,或者在腳本引入代碼上添加defer或async屬性,以表明它們是可以異步加載
此外,在HTTP1.X中,同一個(gè)域名下并行資源請求的數(shù)量是很有限的,不同瀏覽器的限制不一定一致,對于這一點(diǎn)又衍生出多條優(yōu)化方案:
將資源請求合并,如利用雪碧圖將多個(gè)圖片合為一個(gè)圖片使用
啟用靜態(tài)資源專用域名,擴(kuò)大瀏覽器同時(shí)進(jìn)行資源請求的數(shù)量
雖然CSS資源的加載并不影響JS資源的加載,但是卻會影響JS的執(zhí)行:
這是因?yàn)镴S內(nèi)部可能會執(zhí)行一些依賴于CSS樣式的操作,如獲得元素尺寸,所以JS執(zhí)行前,瀏覽器務(wù)必保證CSS資源下載和解析完成。所以如果有需要提前執(zhí)行的JS腳本,可以將JS引入置于CSS引入之前。
解析
瀏覽器解析HTML文檔主要包含如下幾個(gè)部分:
- 將HTML文檔解析為DOM Tree,是由DOM元素以及屬性節(jié)點(diǎn)組成,樹的根是Document對象
- 將CSS解析為樣式表對象,是CSS選擇器以及對象CSS規(guī)則的集合
- JS腳本解析,可能會對DOM Tree以及元素樣式進(jìn)行更改,并最終反映到DOM Tree和CSS樣式表對象的更新里面
渲染
此過程即為瀏覽器構(gòu)建渲染樹(Render Tree)的過程,渲染樹是DOM樹的可視化表示,是DOM數(shù)與樣式表對象的結(jié)合,所以DOM樹中一些不可見元素不會插入到渲染樹中,如 <head> 或 display:none 的元素等。一些脫離了文檔流的元素(使用了絕對或浮動的元素等)在兩棵樹上的位置不完全一致,渲染樹會標(biāo)識出真實(shí)的位置,而只在原有位置放置占位標(biāo)識。
渲染過程中,瀏覽器要為每個(gè)元素找出在樣式表對象中定義的不定數(shù)量的樣式規(guī)則。這里就需要指出CSS選擇器匹配的問題,多重選擇器是從右向左進(jìn)行匹配,選擇器太深不利于樣式查找。
渲染樹創(chuàng)建完畢后,則繼續(xù)進(jìn)行布局(Layout)。這個(gè)過程根據(jù)渲染對象的信息,計(jì)算出每個(gè)渲染對象的位置、尺寸,將其放置在瀏覽器窗口的正確位置。如果在布局完成之后又進(jìn)行了DOM修改,這時(shí)候可能需要重新布局。每個(gè)渲染對象都有其自身的布局方法,同樣渲染樹的布局可以是全局也可以是局部,比如窗口尺寸改變或修改根元素尺寸或字體大小等,將會帶來全局的重新布局,如果只改變了內(nèi)部某一塊渲染對象,則可能帶來一個(gè)局部重新布局。
布局完成之后,瀏覽器會遍歷上步獲得的呈現(xiàn)樹,不斷調(diào)用呈現(xiàn)器的繪制方法,將對應(yīng)呈現(xiàn)器的內(nèi)容顯示在屏幕上。
下圖指示了WebKit瀏覽器解析渲染的過程
此外,還需要說明兩個(gè)概念, 重繪(Repaint) 和 回流(Reflow)
Repaint: 不影響布局的屬性變更,如背景色
Reflow: 元素的幾何尺寸變了,需要重新驗(yàn)證并計(jì)算Render Tree。是Render Tree的一部分或全部發(fā)生了變化。這就是Reflow,或是Layout
Reflow的成本比Repaint的成本高得多的多。DOM Tree里的每個(gè)結(jié)點(diǎn)都會有reflow方法,一個(gè)結(jié)點(diǎn)的reflow很有可能導(dǎo)致子結(jié)點(diǎn),甚至父點(diǎn)以及同級結(jié)點(diǎn)的reflow。在一些高性能的電腦上也許還沒什么,但是如果reflow發(fā)生在手機(jī)上,那么這個(gè)過程是非常痛苦和耗電的。 所以,下面這些動作有很大可能會是成本比較高的。
當(dāng)你增加、刪除、修改DOM結(jié)點(diǎn)時(shí),會導(dǎo)致Reflow或Repaint
當(dāng)你移動DOM的位置,或是搞個(gè)動畫的時(shí)候。
當(dāng)你修改CSS樣式的時(shí)候。
當(dāng)你Resize窗口的時(shí)候(移動端沒有這個(gè)問題),或是滾動的時(shí)候。
當(dāng)你修改網(wǎng)頁的默認(rèn)字體時(shí)。
display:none會觸發(fā)reflow,而visibility:hidden只會觸發(fā)repaint,因?yàn)闆]有發(fā)現(xiàn)位置變化。
基本上來說,reflow有如下的幾個(gè)原因:
Initial: 網(wǎng)頁初始化的時(shí)候。
Incremental: 一些Javascript在操作DOM Tree時(shí)。
Resize: 其些元件的尺寸變了。
Style Change: 如果CSS的屬性發(fā)生變化了。
Dirty: 幾個(gè)Incremental的reflow發(fā)生在同一個(gè)frame的子樹上。
瀏覽器對于頻繁的重繪和回流做了一些優(yōu)化,比如,瀏覽器會把回流操作積攢一批,然后做一次回流,這又叫異步回流或增量異步回流。但是有些情況瀏覽器是不會這么做的,比如:resize窗口,改變了頁面默認(rèn)的字體,等。對于這些操作,瀏覽器會馬上進(jìn)行回流。
頁面渲染優(yōu)化
雖然瀏覽器已經(jīng)極盡所能會對頁面渲染做一些針對性的優(yōu)化,但是在了解瀏覽器的文檔加載渲染和解析過程之后,我們需要對自己編寫的代碼做一些相對應(yīng)的優(yōu)化和更佳的實(shí)踐:
簡化DOM層次結(jié)構(gòu)
簡化CSS選擇器結(jié)構(gòu)
腳本后置
少量首屏樣式可內(nèi)聯(lián)
腳本中減少不必要的DOM操作,盡量緩存DOM查找以及DOM樣式信息
盡量通過修改類名或動畫來修改元素樣式
動畫盡量使用在絕對或固定定位元素上
屏幕外不可見部分或滾動時(shí)的動畫盡量停止