通過(guò)Performance面板驗(yàn)證瀏覽器資源加載與渲染機(jī)制
前言
一個(gè)網(wǎng)頁(yè)的核心由HTML、CSS和JavaScript組成,三者協(xié)同工作以呈現(xiàn)內(nèi)容并實(shí)現(xiàn)交互。但瀏覽器如何解析這些資源?加載順序如何影響用戶體驗(yàn)?讀完本本文你將徹底弄懂以下核心知識(shí)
- 為什么需要生成DOM樹(shù)?
- CSS是否會(huì)阻塞HTML解析?是否阻塞頁(yè)面渲染?
- JavaScript是否會(huì)阻塞HTML解析?
- JavaScript的異步加載機(jī)制如何影響解析過(guò)程?
- 如何通過(guò)performance面板驗(yàn)證以上觀點(diǎn)
認(rèn)識(shí)DOM
文檔對(duì)象模型(Document Object Model,簡(jiǎn)稱 DOM),是 W3C 組織推薦的處理可擴(kuò)展置標(biāo)語(yǔ)言的標(biāo)準(zhǔn)編程接口。DOM 把整個(gè)頁(yè)面映射為一個(gè)多層的節(jié)點(diǎn)結(jié)構(gòu),HTML 或 XML 頁(yè)面中的每個(gè)組成部分都是某種類型的節(jié)點(diǎn),這些節(jié)點(diǎn)又包含著不同類型的數(shù)據(jù)。
W3C DOM 由以下三部分組成:
- 核心 DOM - 針對(duì)任何結(jié)構(gòu)化文檔的標(biāo)準(zhǔn)模型
- XML DOM - 針對(duì) XML 文檔的標(biāo)準(zhǔn)模型
- HTML DOM - 針對(duì) HTML 文檔的標(biāo)準(zhǔn)模型
如果覺(jué)得這樣介紹不夠直觀,可以看下這張圖:
圖片
各個(gè)dom節(jié)點(diǎn)組合起來(lái)就形成了一個(gè)樹(shù)狀結(jié)構(gòu),所以我們通常稱之為DOM樹(shù)。
為什么需要DOM樹(shù)?
- 結(jié)構(gòu)化數(shù)據(jù):將HTML標(biāo)簽(如
<div>、<p>)和文本內(nèi)容轉(zhuǎn)化為節(jié)點(diǎn)對(duì)象,以樹(shù)形結(jié)構(gòu)表示標(biāo)簽的父子嵌套關(guān)系。 - JavaScript動(dòng)態(tài)操作的基礎(chǔ):這一過(guò)程解決了原生HTML文本的局限性,允許JavaScript通過(guò)屬性與方法直接操作節(jié)點(diǎn)。
- 渲染過(guò)程的核心輸入:DOM樹(shù)提供內(nèi)容結(jié)構(gòu),CSSOM樹(shù)提供樣式規(guī)則,兩者結(jié)合生成渲染樹(shù)(Render Tree),決定頁(yè)面元素的可見(jiàn)性與布局。
- 安全性:DOM解析階段會(huì)過(guò)濾惡意內(nèi)容。
DOM 是瀏覽器對(duì)頁(yè)面的內(nèi)部表示,也是 Web 開(kāi)發(fā)人員可以通過(guò) JavaScript 交互的數(shù)據(jù)結(jié)構(gòu)和 API。
解析HTML
網(wǎng)絡(luò)線程獲取HTML文件后,瀏覽器才會(huì)開(kāi)始進(jìn)行解析處理生成DOM樹(shù)。
圖片
在這個(gè)過(guò)程中每個(gè)HTML標(biāo)簽都會(huì)被瀏覽器解析成文檔對(duì)象,并且所有的文檔對(duì)象最終都會(huì)被掛在document上
比如:
圖片
并且為了提高解析效率,在解析之前,瀏覽器會(huì)啟動(dòng)一個(gè)預(yù)解析的線程,提前去下載文檔中的外部CSS文件和外部JS文件。
解析CSS
在構(gòu)建DOM的過(guò)程中,如果遇到link標(biāo)簽,當(dāng)把它插入到DOM樹(shù)上后,此時(shí)如果外部的CSS文件還沒(méi)有下載完,主線程也不會(huì)停下來(lái)等待,因?yàn)?/span>下載和解析CSS的工作是在預(yù)解析線程中進(jìn)行的,所以CSS并不會(huì)阻塞html的解析。
解析html的目的是為了生成DOM樹(shù),而解析CSS的目的同樣是為了生成CSSOM樹(shù),兩者都是為了轉(zhuǎn)換成瀏覽器能夠理解的結(jié)構(gòu),也可以方便javascript的訪問(wèn)。
我們可以通過(guò)document.styleSheets來(lái)查看它的結(jié)構(gòu):
圖片
CSSOM結(jié)構(gòu)主要是為了給JavaScript提供操作樣式表的能力,以及提供基礎(chǔ)的樣式信息。
大體上來(lái)說(shuō),CSSOM是一個(gè)建立在web頁(yè)面上的 CSS 樣式的映射,它和DOM類似,但是只針對(duì)CSS而不是HTML,瀏覽器會(huì)將DOM和CSSOM結(jié)合生成渲染樹(shù)。
CSS是否會(huì)阻塞渲染?
雖然CSS并不會(huì)阻塞html的解析,但由于渲染樹(shù)的生成需要CSSOM的參與,所以CSS是會(huì)阻塞頁(yè)面渲染的
真的原因是,如果瀏覽器在CSS檢查之前展示了頁(yè)面,那么每個(gè)頁(yè)面都是沒(méi)有樣式的,等一會(huì)之后又突然有了樣式,整個(gè)頁(yè)面的體驗(yàn)就會(huì)很差。由于CSSOM被用作創(chuàng)建渲染樹(shù),那么如果不能高效的利用CSS會(huì)導(dǎo)致白屏?xí)r間的增加
解析javascript
在構(gòu)建DOM的過(guò)程中,如果遇到script,在默認(rèn)情況下主線程會(huì)停止對(duì)html的解析,轉(zhuǎn)而等待 JS 文件下載好,并將全局代碼解析執(zhí)行完成后,才會(huì)繼續(xù)解析html。這是因?yàn)?JS 代碼的執(zhí)行過(guò)程可能會(huì)修改當(dāng)前的 DOM 樹(shù),所以 DOM 樹(shù)的生成必須暫停。這就是 JS 會(huì)阻塞 HTML 解析的根本原因。
在html5中對(duì)script新增了兩個(gè)屬性可用于異步加載腳本,設(shè)置不同的屬性對(duì)解析HTML文檔也有很大的影響。
異步加載

這里我們要討論的不僅僅是async與defer,還有type=module
默認(rèn)情況
- 在默認(rèn)情況下,script 標(biāo)簽在請(qǐng)求和執(zhí)行的時(shí)候都會(huì)阻塞文檔解析
defer
延遲腳本執(zhí)行:帶有defer屬性的腳本,加載不會(huì)阻塞頁(yè)面的解析和渲染過(guò)程,瀏覽器可以繼續(xù)解析頁(yè)面的其余部分,當(dāng)整個(gè)文檔完成解析后,在觸發(fā)DOMContentLoaded事件之前執(zhí)行這些腳本。
順序執(zhí)行:帶有defer屬性的腳本,盡管是異步加載的,但是它們之間會(huì)保持順序執(zhí)行。
async
非阻塞加載:帶有async屬性的腳本加載是異步的,不會(huì)阻塞HTML文檔的解析,瀏覽器可以繼續(xù)向下解析和渲染。不過(guò),當(dāng)腳本加載完成后,會(huì)立即執(zhí)行腳本內(nèi)的代碼,此時(shí)如果HTML還沒(méi)有解析完成,則會(huì)暫停對(duì)html的解析,從而阻塞頁(yè)面渲染。但如果當(dāng)腳本加載完準(zhǔn)備執(zhí)行之前,html已經(jīng)解析完成,此時(shí)也不會(huì)阻塞頁(yè)面渲染。
執(zhí)行不可控:帶有async屬性的腳本,執(zhí)行是不可控的,因?yàn)闊o(wú)法確定腳本的下載速度與腳本內(nèi)容的執(zhí)行速度,如果存在多個(gè)script async時(shí),他們之間的執(zhí)行的順序也是不可控的,完全取決于各自的下載速度,誰(shuí)先下載完成就先執(zhí)行誰(shuí)。
module
非阻塞加載:帶有type="module"的腳本加載是異步的,這類標(biāo)簽視為ES6模塊來(lái)處理,而ES6模塊是設(shè)計(jì)為異步加載的,當(dāng)瀏覽器遇到此類標(biāo)簽時(shí),會(huì)開(kāi)始異步下載改模塊及其依賴項(xiàng),不會(huì)暫停頁(yè)面的解析和渲染工作,當(dāng)HTML文檔被解析完成后,會(huì)在觸發(fā)DOMContentLoaded事件之前執(zhí)行這些腳本。所以它的表現(xiàn)有點(diǎn)類似defer。
模塊化支持:帶有type="module"的腳本會(huì)自動(dòng)分割成不同的模塊,并且相互之間作用域是隔離的,瀏覽器會(huì)自動(dòng)加載這些模塊,無(wú)需手動(dòng)管理依賴關(guān)系。
支持靜態(tài)導(dǎo)入和動(dòng)態(tài)導(dǎo)入:可以使用import語(yǔ)句靜態(tài)地導(dǎo)入其它模塊,這些導(dǎo)入的模塊加載時(shí)自動(dòng)解析和執(zhí)行。還可以使用import()函數(shù)動(dòng)態(tài)地導(dǎo)入模塊,根據(jù)需要在運(yùn)行時(shí)加載模塊,進(jìn)一步控制模塊的加載和執(zhí)行時(shí)機(jī)。
module && async
表現(xiàn)類似async
通過(guò)performance驗(yàn)證
實(shí)驗(yàn)代碼如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="./a.css" />
<script>
console.log('【readystatechange】', document.readyState)
document.addEventListener('readystatechange', (e) => {
console.log('【readystatechange】', document.readyState)
})
window.addEventListener('load', (e) => {
console.log('【load】')
})
document.addEventListener('DOMContentLoaded', (e) => {
console.log('【DOMContentLoaded】')
})
</script>
<script defer src="./a.js"></script>
<script async src="./b.js"></script>
<script src="./c.js"></script>
</head>
<body>
<div class="container"></div>
</body>
</html>網(wǎng)絡(luò)
頁(yè)面中分別引入了一個(gè)css資源,三個(gè)JS資源以及一個(gè)內(nèi)聯(lián)腳本,我們可以看下各資源的加載情況
圖片
首先肯定是先加載html文件,而html中的css與js資源會(huì)以他們?cè)谖臋n中的順序依次進(jìn)行請(qǐng)求加載,由于內(nèi)聯(lián)腳本無(wú)需再發(fā)起請(qǐng)求,所以在網(wǎng)絡(luò)模塊中也不會(huì)有它的存在。
注意看紅線位置,這是html開(kāi)始解析的時(shí)間點(diǎn),從network模塊上看,內(nèi)部的css與js資源竟然在這之前就發(fā)起了請(qǐng)求,這也就驗(yàn)證了為了加速,瀏覽器的預(yù)加載掃描器會(huì)同時(shí)運(yùn)行,如果在 html 中存在 <link>、<script>、img 等標(biāo)簽,預(yù)加載掃描器會(huì)把這些請(qǐng)求傳遞給瀏覽器進(jìn)程中的網(wǎng)絡(luò)線程進(jìn)行相關(guān)資源的下載。
從圖中我們可以看到,a.css與c.js文件右上角都出現(xiàn)了紅色標(biāo)注,這是代表這兩個(gè)文件都會(huì)阻塞頁(yè)面的渲染
圖片
為了更清晰的了解各模塊的加載解析與渲染之間的關(guān)系,我們可以查看下方的主線程模塊
主線程
圖片
可以看到html的解析、css的解析、腳本的執(zhí)行、頁(yè)面渲染等都發(fā)生在主線程
parse html
在瀏覽器渲染引擎內(nèi)部,有一個(gè)叫HTML 解析器(HTMLParser)的模塊,它負(fù)責(zé)將HTML字節(jié)流轉(zhuǎn)換為DOM結(jié)構(gòu)。HTML Standard規(guī)范定義了瀏覽器渲染HTML為DOM的方法。
??需要注意的是HTML解析器并不是等整個(gè)文檔加載完成之后再解析的,而是網(wǎng)絡(luò)進(jìn)程加載了多少數(shù)據(jù),HTML解析器就解析多少數(shù)據(jù)。
圖片
注意看,第一次解析html的范圍是0-24,解析過(guò)程遇到了css資源,等css資源下載完成后會(huì)開(kāi)始解析css(這個(gè)過(guò)程不會(huì)阻塞html的解析)
parse stylesheet
圖片
Evaluate script
再往后就遇到了內(nèi)聯(lián)腳本,這個(gè)時(shí)候會(huì)停下來(lái)解析執(zhí)行JS(這才是導(dǎo)致html解析暫停的根本原因)
圖片
等腳本執(zhí)行完后會(huì)繼續(xù)解析html
圖片
注意range,跟上一次parse html剛好接上了
接著往后會(huì)依次遇到a、b、c三個(gè)腳本,由于c沒(méi)有添加任何異步屬性,所以c會(huì)率先開(kāi)始執(zhí)行(此時(shí)會(huì)阻塞html的解析)
圖片
執(zhí)行完成后會(huì)繼續(xù)解析html
圖片
等解析完成后會(huì)開(kāi)始執(zhí)行標(biāo)記了defer的a.js(a.js在這之前就已加載完成)
圖片
最后標(biāo)記了async的b.js加載完成,會(huì)立即執(zhí)行
圖片


























