為什么要了解瀏覽器加載、解析、渲染這個(gè)過程?
好,我們先說一下,為什么要了解這些呢?如果想寫出一個(gè)最佳實(shí)踐的頁面,就要好好了解。
- 了解瀏覽器如何進(jìn)行加載,可以在引用外部樣式文件,外部js時(shí),將他們放到合適的位置,使瀏覽器以最快的速度將文件加載完畢。
- 了解瀏覽器如何進(jìn)行解析,可以在構(gòu)建DOM結(jié)構(gòu),組織css選擇器時(shí),選擇最優(yōu)的寫法,提高瀏覽器的解析速率。
- 了解瀏覽器如何進(jìn)行渲染,明白渲染的過程,在設(shè)置元素屬性,編寫js文件時(shí),可以減少”reflow“”repaint“的消耗。
一、瀏覽器的主要功能
瀏覽器的主要功能是將用戶選擇的web資源呈現(xiàn)出來,它需要從服務(wù)器請(qǐng)求資源,并將其顯示在瀏覽器窗口中,資源的格式通常是HTML,也包括PDF、image及其他格式。用戶用URI(Uniform Resource Identifier統(tǒng)一資源標(biāo)識(shí)符)來指定所請(qǐng)求資源的位置,通過DNS查詢,將網(wǎng)址轉(zhuǎn)換為IP地址。整個(gè)瀏覽器工作的流程,之前文章中有論述:
1、瀏覽器(客戶端)進(jìn)行了地址解析。
2、將解析出的域名進(jìn)行dns解析。
3、通過ip尋址和arp,找到目標(biāo)(服務(wù)器)地址。
4、進(jìn)行tcp三次握手,建立tcp連接。
5、瀏覽器發(fā)送數(shù)據(jù),等待服務(wù)器響應(yīng)。
6、服務(wù)器處理請(qǐng)求,并對(duì)請(qǐng)求做出響應(yīng)。
7、關(guān)閉TCP連接。
8、渲染頁面。
那么,一個(gè)頁面,究竟是如何從我們輸入一個(gè)網(wǎng)址到最后完整的呈現(xiàn)在我們面前的呢?還需要了解一下瀏覽器是如何渲染的:
二、瀏覽器的渲染
下面是渲染引擎在取得內(nèi)容之后的基本流程:
解析html以構(gòu)建dom樹 -> 構(gòu)建render樹 -> 布局render樹 -> 繪制render樹

所以,瀏覽器會(huì)解析三個(gè)東西:
(1) HTML/SVG/XHTML,解析這三種文件會(huì)產(chǎn)生一個(gè) DOM Tree。
(2) CSS,解析 CSS 會(huì)產(chǎn)生 CSS 規(guī)則樹。
(3) Javascript腳本,主要是通過 DOM API 和 CSSOM API 來操作 DOM Tree 和 CSS Rule Tree.
圖中的過程,我是按照自己的理解來說,如果有誤,歡迎指正。
當(dāng)瀏覽器獲得一個(gè)html文件時(shí),會(huì)“自上而下”加載,并在加載過程中進(jìn)行解析渲染。
解析:
- 瀏覽器會(huì)將HTML解析成一個(gè)DOM樹,DOM 樹的構(gòu)建過程是一個(gè)深度遍歷過程:當(dāng)前節(jié)點(diǎn)的所有子節(jié)點(diǎn)都構(gòu)建好后才會(huì)去構(gòu)建當(dāng)前節(jié)點(diǎn)的下一個(gè)兄弟節(jié)點(diǎn)。
- 將CSS解析成 CSS Rule Tree 。
-
根據(jù)DOM樹和CSSOM來構(gòu)造 Rendering Tree。注意:Rendering Tree 渲染樹并不等同于 DOM 樹,因?yàn)橐恍┫?Header 或 display:none 的東西就沒必要放在渲染樹中了。
4.有了Render Tree,瀏覽器已經(jīng)能知道網(wǎng)頁中有哪些節(jié)點(diǎn)、各個(gè)節(jié)點(diǎn)的CSS定義以及他們的從屬關(guān)系。下一步操作稱之為L(zhǎng)ayout,顧名思義就是計(jì)算出每個(gè)節(jié)點(diǎn)在屏幕中的位置。
5.再下一步就是繪制,即遍歷render樹,并使用UI后端層繪制每個(gè)節(jié)點(diǎn)。
重點(diǎn)來了:
上述這個(gè)過程是逐步完成的,為了更好的用戶體驗(yàn),渲染引擎將會(huì)盡可能早的將內(nèi)容呈現(xiàn)到屏幕上,并不會(huì)等到所有的html都解析完成之后再去構(gòu)建和布局render樹。它是解析完一部分內(nèi)容就顯示一部分內(nèi)容,同時(shí),可能還在通過網(wǎng)絡(luò)下載其余內(nèi)容。(這段話是《how browsers work》里面講的,讓我茅塞頓開)
幾個(gè)概念:
(1)Reflow(回流):瀏覽器要花時(shí)間去渲染,當(dāng)它發(fā)現(xiàn)了某個(gè)部分發(fā)生了變化影響了布局,那就需要倒回去重新渲染。
(2)Repaint(重繪):如果只是改變了某個(gè)元素的背景顏色,文字顏色等,不影響元素周圍或內(nèi)部布局的屬性,將只會(huì)引起瀏覽器的repaint,重畫某一部分。
Reflow要比Repaint更花費(fèi)時(shí)間,也就更影響性能。所以在寫代碼的時(shí)候,要盡量避免過多的Reflow。
reflow的原因:
(1)頁面初始化的時(shí)候;
(2)操作DOM時(shí);
(3)某些元素的尺寸變了;
(4)如果 CSS 的屬性發(fā)生變化了。
減少 reflow/repaint
(1)不要一條一條地修改 DOM 的樣式。與其這樣,還不如預(yù)先定義好 css 的 class,然后修改 DOM 的 className。
?。?)不要把 DOM 結(jié)點(diǎn)的屬性值放在一個(gè)循環(huán)里當(dāng)成循環(huán)里的變量。
?。?)為動(dòng)畫的 HTML 元件使用 fixed 或 absoult 的 position,那么修改他們的 CSS 是不會(huì) reflow 的。
?。?)千萬不要使用 table 布局。因?yàn)榭赡芎苄〉囊粋€(gè)小改動(dòng)會(huì)造成整個(gè) table 的重新布局。
HTML頁面加載和解析流程
1. 用戶輸入網(wǎng)址(假設(shè)是個(gè)html頁面,并且是第一次訪問),瀏覽器向服務(wù)器發(fā)出請(qǐng)求,服務(wù)器返回html文件;
2. 瀏覽器開始載入html代碼,發(fā)現(xiàn)<head>標(biāo)簽內(nèi)有一個(gè)<link>標(biāo)簽引用外部CSS文件;
3. 瀏覽器又發(fā)出CSS文件的請(qǐng)求,服務(wù)器返回這個(gè)CSS文件;
4. 瀏覽器繼續(xù)載入html中<body>部分的代碼,并且CSS文件已經(jīng)拿到手了,可以開始渲染頁面了;
5. 瀏覽器在代碼中發(fā)現(xiàn)一個(gè)<img>標(biāo)簽引用了一張圖片,向服務(wù)器發(fā)出請(qǐng)求。此時(shí)瀏覽器不會(huì)等到圖片下載完,而是繼續(xù)渲染后面的代碼;
6. 服務(wù)器返回圖片文件,由于圖片占用了一定面積,影響了后面段落的排布,因此瀏覽器需要回過頭來重新渲染這部分代碼;
7. 瀏覽器發(fā)現(xiàn)了一個(gè)包含一行Javascript代碼的<script>標(biāo)簽,趕快運(yùn)行它;
8. Javascript腳本執(zhí)行了這條語句,它命令瀏覽器隱藏掉代碼中的某個(gè)<div> (style.display=”none”)。突然少了這么一個(gè)元素,瀏覽器不得不重新渲染這部分代碼;
9. 終于等到了</html>的到來,瀏覽器淚流滿面……
10. 等等,還沒完,用戶點(diǎn)了一下界面中的“換膚”按鈕,Javascript讓瀏覽器換了一下<link>標(biāo)簽的CSS路徑;
11. 瀏覽器召集了在座的各位<div><span><ul><li>們,“大伙兒收拾收拾行李,咱得重新來過……”,瀏覽器向服務(wù)器請(qǐng)求了新的CSS文件,重新渲染頁面。
與討論主題相關(guān)的其他思考
編寫CSS時(shí)應(yīng)該注意:
CSS選擇符是從右到左進(jìn)行匹配的。從右到左!所以,#nav li 我們以為這是一條很簡(jiǎn)單的規(guī)則,秒秒鐘就能匹配到想要的元素,但是,但是,但是,是從右往左匹配啊,所以,會(huì)去找所有的li,然后再去確定它的父元素是不是#nav。,因此,寫css的時(shí)候需要注意:
1. dom深度盡量淺。
2. 減少inline javascript、css的數(shù)量。
3. 使用現(xiàn)代合法的css屬性。
4. 不要為id選擇器指定類名或是標(biāo)簽,因?yàn)閕d可以唯一確定一個(gè)元素。
5. 避免后代選擇符,盡量使用子選擇符。原因:子元素匹配符的概率要大于后代元素匹配符。后代選擇符;#tp p{} 子選擇符:#tp>p{}
6. 避免使用通配符,舉一個(gè)例子,.mod .hd *{font-size:14px;} 根據(jù)匹配順序,將首先匹配通配符,也就是說先匹配出通配符,然后匹配.hd(就是要對(duì)dom樹上的所有節(jié)點(diǎn)進(jìn)行遍歷他的父級(jí)元素),然后匹配.mod,這樣的性能耗費(fèi)可想而知.
關(guān)于script標(biāo)簽的位置
現(xiàn)在,我們大都會(huì)將script標(biāo)簽放在body結(jié)束標(biāo)簽之前,那原因是什么呢?我今天也做了一個(gè)測(cè)試。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>測(cè)試js代碼位置</title>
<script type="text/javascript">
var item = document.getElementById("item");
cosole.log(item);
</script>
</head>
<body>
<div id="item" width="100px" height="100px">
你好
</div>
</body>
</html>
上述代碼中有一段js代碼,要在控制臺(tái)打印一個(gè)元素,我把script標(biāo)簽放在head里,控制臺(tái)里打印出來的是null。

我又把js代碼放在body結(jié)束標(biāo)簽之前,打印出來的就是div元素了

所以,通過這個(gè)簡(jiǎn)單的例子我們可以看到,js代碼在加載完后,是立即執(zhí)行的。
我又做了一個(gè)測(cè)試,在js代碼里面寫了一個(gè)死循環(huán),把它放在head標(biāo)簽中,
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>測(cè)試js代碼位置</title>
<script type="text/javascript">
var item = document.getElementById("item");
while(true){
console.log(1);
}
</script>
</head>
<body>
<div id="item" width="100px" height="100px">
你好
</div>
</body>
</html>
頁面是這樣的:

一直在執(zhí)行那個(gè)打印1的死循環(huán),后面的body都沒有加載渲染出來。所以,這個(gè)小例子,我們可以看出,js的下載和執(zhí)行會(huì)阻塞Dom樹的構(gòu)建。
所以,Javascript的加載和執(zhí)行的特點(diǎn):
(1)載入后馬上執(zhí)行;
(2)執(zhí)行時(shí)會(huì)阻塞頁面后續(xù)的內(nèi)容(包括頁面的渲染、其它資源的下載)。原因:因?yàn)闉g覽器需要一個(gè)穩(wěn)定的DOM樹結(jié)構(gòu),而JS中很有可能有 代碼直接改變了DOM樹結(jié)構(gòu),比如使用 document.write 或 appendChild,甚至是直接使用的location.href進(jìn)行跳轉(zhuǎn),瀏覽器為了防止出現(xiàn)JS修 改DOM樹,需要重新構(gòu)建DOM樹的情況,所以 就會(huì)阻塞其他的下載和呈現(xiàn)。
減少 JavaScript 對(duì)性能的影響的方法:
1. 將所有的script標(biāo)簽放到頁面底部,也就是body閉合標(biāo)簽之前,這能確保在腳本執(zhí)行前頁面已經(jīng)完成了DOM樹渲染。
2. 盡可能地合并腳本。頁面中的script標(biāo)簽越少,加載也就越快,響應(yīng)也越迅速。無論是外鏈腳本還是內(nèi)嵌腳本都是如此。
3. 采用無阻塞下載 JavaScript 腳本的方法:
(1)使用script標(biāo)簽的 defer 屬性(僅適用于 IE 和 Firefox 3.5 以上版本);
(2)使用動(dòng)態(tài)創(chuàng)建的script元素來下載并執(zhí)行代碼;
