(注1:如果有問題歡迎留言探討和橙,一起學(xué)習(xí)撬腾!轉(zhuǎn)載請注明出處,喜歡可以點個贊哦T次洹)
(注2:更多內(nèi)容請查看我的目錄。)
1. 簡介
在前面一篇文章中言秸,講到了用戶從輸入url到看到頁面的過程软能,其中涉及到瀏覽器的工作機制這一塊我們并沒有去詳細(xì)分析迎捺。這篇文章举畸,將對瀏覽器的加載解析渲染機制進行深入地剖析。在這篇文章的寫作過程中凳枝,我參考了網(wǎng)上大量相關(guān)資料抄沮,發(fā)現(xiàn)有不少文章只有文字,卻沒有去深入驗證岖瑰。有些看了似懂非懂叛买,有些甚至互相矛盾。實踐是檢驗真理的唯一標(biāo)準(zhǔn)蹋订,所以在這篇文章中我進行了實踐探索率挣,并在文中放出了詳細(xì)的代碼,大家可以照著這個思路去做更多的探索和驗證露戒。文章寫的倉促椒功,優(yōu)化點還沒有寫完捶箱,也沒有時間去校驗自己的文章。如有錯誤动漾,請大家不吝指正丁屎。寫這篇文章前看了大量的參考資料,參考文章若有遺漏旱眯,請聯(lián)系我晨川,如果圖片不允許引用,也請聯(lián)系我刪除删豺。
2. 瀏覽器的高級結(jié)構(gòu)
瀏覽器的主要組件包括:
用戶界面(user interface)- 包括地址欄共虑、后退/前進按鈕、書簽?zāi)夸浀群鹆郏簿褪悄闼吹降某擞脕盹@示你所請求頁面的主窗口之外的其他部分看蚜。
瀏覽器引擎(browser engine)- 用來查詢及操作渲染引擎的接口。
渲染引擎(rendering engine)- 用來顯示請求的內(nèi)容赔桌,例如供炎,如果請求內(nèi)容為html,它負(fù)責(zé)解析html及css疾党,并將解析后的結(jié)果顯示出來音诫。
網(wǎng)絡(luò)(Networking)- 用來完成網(wǎng)絡(luò)調(diào)用,例如http請求雪位,它具有平臺無關(guān)的接口竭钝,可以在不同平臺上工作。
UI 后端(UI backend)- 用來繪制類似組合選擇框及對話框等基本組件雹洗,具有不特定于某個平臺的通用接口香罐,底層使用操作系統(tǒng)的用戶接口。
JS解釋器(JavaScript interpreter)- 用來解釋執(zhí)行JS代碼时肿。
數(shù)據(jù)存儲(Data storage)- 屬于持久層庇茫,瀏覽器需要在硬盤中保存類似cookie的各種數(shù)據(jù),HTML5定義了web database技術(shù)螃成,這是一種輕量級完整的客戶端存儲技術(shù)旦签。
需要注意的是,不同于大部分瀏覽器寸宏,Chrome為每個Tab分配了各自的渲染引擎實例宁炫,每個Tab就是一個獨立的進程。
3.瀏覽器份額和渲染引擎
瀏覽器種類眾多氮凝,其市場份額如下:
圖片摘自tatcounter羔巢,顯示是2018年2月份的瀏覽器市場份額。
關(guān)于不同瀏覽器使用的內(nèi)核,大家有興趣的話可以閱讀這篇文章(五大主流瀏覽器內(nèi)核的源起以及國內(nèi)各大瀏覽器內(nèi)核總結(jié))竿秆√砍簦可以看到目前為止,webkit內(nèi)核仍然是主流袍辞。本篇文章將基于webkit來討論瀏覽器工作原理鞋仍。
渲染引擎是單線程的,除了網(wǎng)絡(luò)操作以外搅吁,幾乎所有的事情都在單一的線程中處理威创,在Firefox和Safari中,這是瀏覽器的主線程谎懦,Chrome中這是tab的主線程肚豺。
網(wǎng)絡(luò)操作由幾個并行線程執(zhí)行,并行連接的個數(shù)是受限的(通常是2-6個)界拦。
4. 主流程
渲染引擎首先通過網(wǎng)絡(luò)獲得所請求文檔的內(nèi)容吸申,通常以8K分塊的方式完成。
下面是渲染引擎在取得內(nèi)容之后的基本流程:
解析html以構(gòu)建dom樹->構(gòu)建render樹->布局render樹->繪制render樹
關(guān)于webkit的主流程享甸,或者準(zhǔn)確說頁面的加載解析渲染流程截碴,大家可以參考一下三幅圖:
渲染引擎開始解析HTML/SVG/XHTML,并將標(biāo)簽轉(zhuǎn)化為dom tree中的dom節(jié)點蛉威。接著日丹,它解析外部CSS文件及style標(biāo)簽中的樣式信息生成rule tree。dom tree和rule tree結(jié)合生成render tree蚯嫌。
Render tree由一些包含有顏色和大小等屬性的矩形組成哲虾,它們將被按照正確的順序顯示到屏幕上。
Render tree構(gòu)建好了之后择示,將會執(zhí)行布局過程束凑,它將確定每個節(jié)點在屏幕上的確切坐標(biāo)。再下一步就是繪制栅盲,即遍歷render tree汪诉,并使用UI后端層繪制每個節(jié)點。
值得注意的是剪菱,這個過程是逐步完成的摩瞎,為了更好的用戶體驗拴签,渲染引擎將會盡可能早的將內(nèi)容呈現(xiàn)到屏幕上孝常,并不會等到所有的html都解析完成之后再去構(gòu)建和布局render tree。它是解析完一部分內(nèi)容就顯示一部分內(nèi)容蚓哩,同時构灸,可能還在通過網(wǎng)絡(luò)下載其余內(nèi)容。
5. html下載解析
渲染引擎首先通過網(wǎng)絡(luò)獲得所請求文檔的內(nèi)容岸梨,通常以8K分塊的方式完成喜颁。
html下載完成以后稠氮。瀏覽器的html paser開始對html從上至下進行解析生成DOM tree。
當(dāng)遇到以下情況時半开,DOM樹的構(gòu)建會被阻塞:
- HTML的響應(yīng)流被阻塞在了網(wǎng)絡(luò)中隔披。
- 有未加載完的腳本。
- 遇到了script節(jié)點寂拆,但是此時還有未加載完的樣式文件奢米。
解析結(jié)束時,瀏覽器將文檔標(biāo)記為可交互的纠永,并開始解析處于延時模式中的腳本——這些腳本在文檔解析后執(zhí)行鬓长。文檔狀態(tài)將被設(shè)置為完成,同時觸發(fā)一個DomContendLoaded事件尝江。
輸出的樹涉波,也就是解析樹,是由DOM元素及屬性節(jié)點組成的炭序。DOM是文檔對象模型的縮寫啤覆,它是html文檔的對象表示,作為html元素的外部接口供js等調(diào)用惭聂。
樹的根是“document”對象城侧。
DOM和標(biāo)簽基本是一一對應(yīng)的關(guān)系,例如彼妻,如下的標(biāo)簽:
<html>
<body>
<p>
Hello DOM
</p>
<div><img src=”example.png” /></div>
</body>
</html>
將會被轉(zhuǎn)換為下面的DOM樹:
6. CSS下載解析
在html解析的過程中嫌佑,遇到style標(biāo)簽會直接解析,而遇到link標(biāo)簽會去加載樣式表侨歉。理論上屋摇,既然樣式表不改變Dom樹,也就沒有必要停下文檔的解析等待它們幽邓,然而炮温,存在一個問題,腳本可能在文檔的解析過程中請求樣式信息牵舵,如果樣式還沒有加載和解析柒啤,腳本將得到錯誤的值,顯然這將會導(dǎo)致很多問題畸颅,這看起來是個邊緣情況担巩,但確實很常見。Firefox在存在樣式表還在加載和解析時阻塞所有的腳本没炒,而chrome只在當(dāng)腳本試圖訪問某些可能被未加載的樣式表所影響的特定的樣式屬性時才阻塞這些腳本涛癌。
這里的阻塞js,是指阻塞其加載,還是阻塞其執(zhí)行呢拳话?稍后我們具體分析一下先匪。
Webkit使用Flex和Bison解析生成器從CSS語法文件中自動生成解析器。Bison創(chuàng)建一個自底向上的解析器弃衍,F(xiàn)irefox使用自頂向下解析器呀非。它們都是將每個css文件解析為樣式表對象,每個對象包含css規(guī)則镜盯,css規(guī)則對象包含選擇器和聲明對象姜钳,以及其他一些符合css語法的對象。
7.腳本下載解析執(zhí)行
web的模式是同步的形耗,開發(fā)者希望解析到一個script標(biāo)簽時立即解析執(zhí)行腳本哥桥,并阻塞文檔的解析直到腳本執(zhí)行完。如果腳本是外引的激涤,則網(wǎng)絡(luò)必須先請求到這個資源——這個過程也是同步的拟糕,會阻塞文檔的解析直到資源被請求到。這個模式保持了很多年倦踢,并且在html4及html5中都特別指定了送滞。開發(fā)者可以將腳本標(biāo)識為defer,以使其不阻塞文檔解析辱挥,并在文檔解析結(jié)束后執(zhí)行犁嗅。Html5增加了標(biāo)記腳本為異步的選項,以使腳本的解析執(zhí)行使用另一個線程晤碘。
Webkit和Firefox都做了預(yù)解析的優(yōu)化褂微,當(dāng)執(zhí)行腳本時,另一個線程解析剩下的文檔园爷,并加載后面需要通過網(wǎng)絡(luò)加載的資源宠蚂。這種方式可以使資源并行加載從而使整體速度更快。需要注意的是童社,預(yù)解析并不改變Dom樹求厕,它將這個工作留給主解析過程,自己只解析外部資源的引用扰楼,比如外部腳本呀癣、樣式表及圖片。
8. 構(gòu)建render tree
當(dāng)Dom樹構(gòu)建完成時弦赖,瀏覽器開始構(gòu)建另一棵樹——渲染樹项栏。渲染樹由元素顯示序列中的可見元素組成,它是文檔的可視化表示腾节,構(gòu)建這棵樹是為了以正確的順序繪制文檔內(nèi)容忘嫉。
每個渲染對象用一個和該節(jié)點的css盒模型相對應(yīng)的矩形區(qū)域來表示,正如css2所描述的那樣案腺,它包含諸如寬庆冕、高和位置之類的幾何信息。盒模型的類型受該節(jié)點相關(guān)的display樣式屬性的影響劈榨。
渲染對象和Dom元素相對應(yīng)访递,但這種對應(yīng)關(guān)系不是一對一的,不可見的Dom元素不會被插入渲染樹同辣,例如head元素拷姿。另外,display屬性為none的元素也不會在渲染樹中出現(xiàn)(visibility屬性為hidden的元素將出現(xiàn)在渲染樹中)旱函。
還有一些Dom元素對應(yīng)幾個可見對象响巢,它們一般是一些具有復(fù)雜結(jié)構(gòu)的元素,無法用一個矩形來描述棒妨。例如踪古,select元素有三個渲染對象——一個顯示區(qū)域、一個下拉列表及一個按鈕券腔。同樣伏穆,當(dāng)文本因為寬度不夠而折行時,新行將作為額外的渲染元素被添加纷纫。另一個多個渲染對象的例子是不規(guī)范的html枕扫,根據(jù)css規(guī)范,一個行內(nèi)元素只能僅包含行內(nèi)元素或僅包含塊狀元素辱魁,在存在混合內(nèi)容時烟瞧,將會創(chuàng)建匿名的塊狀渲染對象包裹住行內(nèi)元素。
一些渲染對象和所對應(yīng)的Dom節(jié)點不在樹上相同的位置染簇,例如燕刻,浮動和絕對定位的元素在文本流之外,在兩棵樹上的位置不同剖笙,渲染樹上標(biāo)識出真實的結(jié)構(gòu)卵洗,并用一個占位結(jié)構(gòu)標(biāo)識出它們原來的位置。
9. html弥咪,css过蹂,js的阻塞問題分析
前面幾節(jié),我們講到了dom tree聚至,cssom或者render tree的構(gòu)建過程中酷勺,可能會被css的加載解析或者js的加載解析執(zhí)行所阻塞,另外css資源和js資源之間也可能產(chǎn)生阻塞“夤現(xiàn)在我脆诉,我們來詳細(xì)分析一下這些阻塞情況甚亭。
我們先建立幾個文件,如下:
<!--test.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>test</title>
<script defer src="./test.js"></script>
<style>
div {
width: 100px;
height: 100px;
background: red;
}
</style>
<link rel="stylesheet" href="./test.css?defer=true">
</head>
<body>
<div></div>
</body>
</html>
/*test.css*/
div {
background: blue;
}
// test.js
const div = document.getElementsByTagName('div')[0];
console.log(div);
// server.js
var http = require('http');
var URL = require('url');
var fs = require('fs');
var server = http.createServer(function (req, res) {
if (req.method != 'GET') {
return res.end('send me a get request\n');
} else {
var url = URL.parse(req.url, true);
var params = url.query;
if (url.pathname === '/test.html') {
res.writeHead(200, {'Content-Type': 'text/html'});
fs.createReadStream('test.html').pipe(res);
} else if (url.pathname === '/test.css') {
res.writeHead(200, {'Content-Type': 'text/css'});
if (params.defer) {
setTimeout(function(){fs.createReadStream('test.css').pipe(res)}, 3000);
} else {
fs.createReadStream('test.css').pipe(res);
}
} else if (url.pathname === '/test.js') {
res.writeHead(200, {'Content-Type': 'application/javascript'});
if (params.defer) {
setTimeout(function(){fs.createReadStream('test.js').pipe(res)}, 3000);
} else {
fs.createReadStream('test.js').pipe(res);
}
}
}
});
server.listen(8888);
console.log('sever start');
9.1 css的阻塞特性
進入該文件夾击胜,運行命令node server.js亏狰。打開localhost:8888/test.html,會發(fā)現(xiàn)控制臺打印div以后3秒頁面才出現(xiàn)一個藍色方塊偶摔。
我們來分析一下暇唾,defer 屬性用來通知瀏覽器該腳本將在文檔完成解析后,觸發(fā) DOMContentLoaded 事件前執(zhí)行辰斋。設(shè)置這個屬性策州,能保證 DOM 解析后馬上打印出 div ,也就是說控制臺打印div說明dom tree構(gòu)建完畢宫仗。從gif圖可以看出css文件的加載沒有阻塞DOM tree的構(gòu)建够挂,但是阻塞了render tree的構(gòu)建。
如果 CSS 不會阻塞頁面阻塞渲染藕夫,那么 CSS 文件下載之前下硕,瀏覽器就會渲染出一個紅色的 div ,之后再變成藍色汁胆。瀏覽器的這個策略其實很明智的梭姓,想象一下,如果沒有這個策略嫩码,頁面首先會呈現(xiàn)出一個原始的模樣誉尖,待 CSS 下載完之后又突然變了一個模樣。用戶體驗可謂極差铸题,而且渲染是有成本的铡恕。
因此,基于性能與用戶體驗這兩點的考慮丢间,瀏覽器會盡量減少渲染的次數(shù)探熔, CSS 順理成章地阻塞頁面渲染。
現(xiàn)在烘挫,將test.html修改如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>test</title>
<link rel="stylesheet" href="./test.css?defer=true">
<script src="./test.js"></script>
<style>
div {
width: 100px;
height: 100px;
background: red;
}
</style>
</head>
<body>
<div></div>
</body>
</html>
會有如下情況發(fā)生:
會發(fā)現(xiàn)诀艰,css文件在js文件之前時,css和js文件雖然都下載了饮六,但是js的執(zhí)行被阻塞了(網(wǎng)上很多blog說這里css阻塞了js的加載是不對的其垄,應(yīng)該是阻塞了js的執(zhí)行),導(dǎo)致DOM tree的構(gòu)建被阻塞了卤橄。
這里绿满,我們稍作修改,給script加上defer
<script defer src="./test.js"></script>
這里窟扑,由于script延遲執(zhí)行喇颁,所以就不會阻塞DOM tree的構(gòu)建了漏健。
所以,我們總結(jié)一下:
- css如果在js之前橘霎,會阻塞js的執(zhí)行蔫浆,從而阻塞DOM tree構(gòu)建
- 要想不阻塞DOM tree構(gòu)建,需要將js在body底部或者使用defer
9.2 js阻塞
我們將test.html修改如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>test</title>
<style>
div {
width: 100px;
height: 100px;
background: red;
}
</style>
</head>
<body>
<div></div>
<script src="./test.js?defer=true"></script>
<link rel="stylesheet" href="./test.css">
<div></div>
</body>
</html>
會有如下情景:
可以看到茎毁,test.js的加載并沒有阻塞test.css的加載克懊,這是由于瀏覽器的預(yù)解析優(yōu)化忱辅,會新開一個線程預(yù)加載后續(xù)資源七蜘。但是開始在頁面只有一個DIV,說明DOM tree構(gòu)建確實被阻塞了墙懂。而且在test.jss執(zhí)行過程中橡卤,瀏覽器已經(jīng)將渲染好的一個紅色div呈現(xiàn)給了用戶。
因為瀏覽器不知道腳本的內(nèi)容损搬,因而碰到腳本時碧库,只好先渲染頁面,確保腳本能獲取到最新的DOM元素信息巧勤,盡管腳本可能不需要這些信息嵌灰。
9.3 阻塞總結(jié)
我們分析如上幾種情況,總結(jié)如下:
html解析的過程中遇到script時颅悉,如果是嵌入腳本沽瞭,會執(zhí)行并阻塞dom tree構(gòu)建;如果是外鏈JS腳本剩瓶,則會進行加載后執(zhí)行驹溃,并阻塞dom tree構(gòu)建。但不管怎樣延曙,由于瀏覽器的預(yù)解析優(yōu)化豌鹤,會新開一個線程加載后續(xù)資源。并且枝缔,為了確保js能拿到最新的DOM元素信息 CSSOM信息布疙,js執(zhí)行前會等待css加載完畢并渲染頁面。
10. 總結(jié)
看到這里愿卸,想必大家對瀏覽器加載解析渲染機制已經(jīng)有了比較清晰的認(rèn)識拐辽。下一篇,我們將對照這篇文章分析一下這個過程中可以幫助提高性能的優(yōu)化點擦酌。
參考
http://taligarsiel.com/Projects/howbrowserswork1.htm
[譯]How browsers work
了解html頁面的渲染過程
瀏覽器加載網(wǎng)頁時的過程是什么俱诸?-creative web developer回答
瀏覽器加載、解析赊舶、渲染的過程
漲知識睁搭!原來CSS與JS是這樣阻塞DOM解析和渲染的
補充:
http://www.reibang.com/p/e4a75cb6f268
https://www.cnblogs.com/echo-hui/p/9231031.html