(注1:如果有問題歡迎留言探討,一起學習!轉載請注明出處,喜歡可以點個贊哦?。?br> (注2:更多內容請查看我的目錄。)
1. 簡介
在前面一篇文章中,講到了用戶從輸入url到看到頁面的過程,其中涉及到瀏覽器的工作機制這一塊我們并沒有去詳細分析。這篇文章,將對瀏覽器的加載解析渲染機制進行深入地剖析。在這篇文章的寫作過程中,我參考了網上大量相關資料,發現有不少文章只有文字,卻沒有去深入驗證。有些看了似懂非懂,有些甚至互相矛盾。實踐是檢驗真理的唯一標準,所以在這篇文章中我進行了實踐探索,并在文中放出了詳細的代碼,大家可以照著這個思路去做更多的探索和驗證。文章寫的倉促,優化點還沒有寫完,也沒有時間去校驗自己的文章。如有錯誤,請大家不吝指正。寫這篇文章前看了大量的參考資料,參考文章若有遺漏,請聯系我,如果圖片不允許引用,也請聯系我刪除。
2. 瀏覽器的高級結構
瀏覽器的主要組件包括:
用戶界面(user interface)- 包括地址欄、后退/前進按鈕、書簽目錄等,也就是你所看到的除了用來顯示你所請求頁面的主窗口之外的其他部分。
瀏覽器引擎(browser engine)- 用來查詢及操作渲染引擎的接口。
渲染引擎(rendering engine)- 用來顯示請求的內容,例如,如果請求內容為html,它負責解析html及css,并將解析后的結果顯示出來。
網絡(Networking)- 用來完成網絡調用,例如http請求,它具有平臺無關的接口,可以在不同平臺上工作。
UI 后端(UI backend)- 用來繪制類似組合選擇框及對話框等基本組件,具有不特定于某個平臺的通用接口,底層使用操作系統的用戶接口。
JS解釋器(JavaScript interpreter)- 用來解釋執行JS代碼。
數據存儲(Data storage)- 屬于持久層,瀏覽器需要在硬盤中保存類似cookie的各種數據,HTML5定義了web database技術,這是一種輕量級完整的客戶端存儲技術。
需要注意的是,不同于大部分瀏覽器,Chrome為每個Tab分配了各自的渲染引擎實例,每個Tab就是一個獨立的進程。
3.瀏覽器份額和渲染引擎
瀏覽器種類眾多,其市場份額如下:
圖片摘自tatcounter,顯示是2018年2月份的瀏覽器市場份額。
關于不同瀏覽器使用的內核,大家有興趣的話可以閱讀這篇文章(五大主流瀏覽器內核的源起以及國內各大瀏覽器內核總結)。可以看到目前為止,webkit內核仍然是主流。本篇文章將基于webkit來討論瀏覽器工作原理。
渲染引擎是單線程的,除了網絡操作以外,幾乎所有的事情都在單一的線程中處理,在Firefox和Safari中,這是瀏覽器的主線程,Chrome中這是tab的主線程。
網絡操作由幾個并行線程執行,并行連接的個數是受限的(通常是2-6個)。
4. 主流程
渲染引擎首先通過網絡獲得所請求文檔的內容,通常以8K分塊的方式完成。
下面是渲染引擎在取得內容之后的基本流程:
解析html以構建dom樹->構建render樹->布局render樹->繪制render樹
關于webkit的主流程,或者準確說頁面的加載解析渲染流程,大家可以參考一下三幅圖:
渲染引擎開始解析HTML/SVG/XHTML,并將標簽轉化為dom tree中的dom節點。接著,它解析外部CSS文件及style標簽中的樣式信息生成rule tree。dom tree和rule tree結合生成render tree。
Render tree由一些包含有顏色和大小等屬性的矩形組成,它們將被按照正確的順序顯示到屏幕上。
Render tree構建好了之后,將會執行布局過程,它將確定每個節點在屏幕上的確切坐標。再下一步就是繪制,即遍歷render tree,并使用UI后端層繪制每個節點。
值得注意的是,這個過程是逐步完成的,為了更好的用戶體驗,渲染引擎將會盡可能早的將內容呈現到屏幕上,并不會等到所有的html都解析完成之后再去構建和布局render tree。它是解析完一部分內容就顯示一部分內容,同時,可能還在通過網絡下載其余內容。
5. html下載解析
渲染引擎首先通過網絡獲得所請求文檔的內容,通常以8K分塊的方式完成。
html下載完成以后。瀏覽器的html paser開始對html從上至下進行解析生成DOM tree。
當遇到以下情況時,DOM樹的構建會被阻塞:
- HTML的響應流被阻塞在了網絡中。
- 有未加載完的腳本。
- 遇到了script節點,但是此時還有未加載完的樣式文件。
解析結束時,瀏覽器將文檔標記為可交互的,并開始解析處于延時模式中的腳本——這些腳本在文檔解析后執行。文檔狀態將被設置為完成,同時觸發一個DomContendLoaded事件。
輸出的樹,也就是解析樹,是由DOM元素及屬性節點組成的。DOM是文檔對象模型的縮寫,它是html文檔的對象表示,作為html元素的外部接口供js等調用。
樹的根是“document”對象。
DOM和標簽基本是一一對應的關系,例如,如下的標簽:
<html>
<body>
<p>
Hello DOM
</p>
<div><img src=”example.png” /></div>
</body>
</html>
將會被轉換為下面的DOM樹:
6. CSS下載解析
在html解析的過程中,遇到style標簽會直接解析,而遇到link標簽會去加載樣式表。理論上,既然樣式表不改變Dom樹,也就沒有必要停下文檔的解析等待它們,然而,存在一個問題,腳本可能在文檔的解析過程中請求樣式信息,如果樣式還沒有加載和解析,腳本將得到錯誤的值,顯然這將會導致很多問題,這看起來是個邊緣情況,但確實很常見。Firefox在存在樣式表還在加載和解析時阻塞所有的腳本,而chrome只在當腳本試圖訪問某些可能被未加載的樣式表所影響的特定的樣式屬性時才阻塞這些腳本。
這里的阻塞js,是指阻塞其加載,還是阻塞其執行呢?稍后我們具體分析一下。
Webkit使用Flex和Bison解析生成器從CSS語法文件中自動生成解析器。Bison創建一個自底向上的解析器,Firefox使用自頂向下解析器。它們都是將每個css文件解析為樣式表對象,每個對象包含css規則,css規則對象包含選擇器和聲明對象,以及其他一些符合css語法的對象。
7.腳本下載解析執行
web的模式是同步的,開發者希望解析到一個script標簽時立即解析執行腳本,并阻塞文檔的解析直到腳本執行完。如果腳本是外引的,則網絡必須先請求到這個資源——這個過程也是同步的,會阻塞文檔的解析直到資源被請求到。這個模式保持了很多年,并且在html4及html5中都特別指定了。開發者可以將腳本標識為defer,以使其不阻塞文檔解析,并在文檔解析結束后執行。Html5增加了標記腳本為異步的選項,以使腳本的解析執行使用另一個線程。
Webkit和Firefox都做了預解析的優化,當執行腳本時,另一個線程解析剩下的文檔,并加載后面需要通過網絡加載的資源。這種方式可以使資源并行加載從而使整體速度更快。需要注意的是,預解析并不改變Dom樹,它將這個工作留給主解析過程,自己只解析外部資源的引用,比如外部腳本、樣式表及圖片。
8. 構建render tree
當Dom樹構建完成時,瀏覽器開始構建另一棵樹——渲染樹。渲染樹由元素顯示序列中的可見元素組成,它是文檔的可視化表示,構建這棵樹是為了以正確的順序繪制文檔內容。
每個渲染對象用一個和該節點的css盒模型相對應的矩形區域來表示,正如css2所描述的那樣,它包含諸如寬、高和位置之類的幾何信息。盒模型的類型受該節點相關的display樣式屬性的影響。
渲染對象和Dom元素相對應,但這種對應關系不是一對一的,不可見的Dom元素不會被插入渲染樹,例如head元素。另外,display屬性為none的元素也不會在渲染樹中出現(visibility屬性為hidden的元素將出現在渲染樹中)。
還有一些Dom元素對應幾個可見對象,它們一般是一些具有復雜結構的元素,無法用一個矩形來描述。例如,select元素有三個渲染對象——一個顯示區域、一個下拉列表及一個按鈕。同樣,當文本因為寬度不夠而折行時,新行將作為額外的渲染元素被添加。另一個多個渲染對象的例子是不規范的html,根據css規范,一個行內元素只能僅包含行內元素或僅包含塊狀元素,在存在混合內容時,將會創建匿名的塊狀渲染對象包裹住行內元素。
一些渲染對象和所對應的Dom節點不在樹上相同的位置,例如,浮動和絕對定位的元素在文本流之外,在兩棵樹上的位置不同,渲染樹上標識出真實的結構,并用一個占位結構標識出它們原來的位置。
9. html,css,js的阻塞問題分析
前面幾節,我們講到了dom tree,cssom或者render tree的構建過程中,可能會被css的加載解析或者js的加載解析執行所阻塞,另外css資源和js資源之間也可能產生阻塞。現在我,我們來詳細分析一下這些阻塞情況。
我們先建立幾個文件,如下:
<!--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,會發現控制臺打印div以后3秒頁面才出現一個藍色方塊。
我們來分析一下,defer 屬性用來通知瀏覽器該腳本將在文檔完成解析后,觸發 DOMContentLoaded 事件前執行。設置這個屬性,能保證 DOM 解析后馬上打印出 div ,也就是說控制臺打印div說明dom tree構建完畢。從gif圖可以看出css文件的加載沒有阻塞DOM tree的構建,但是阻塞了render tree的構建。
如果 CSS 不會阻塞頁面阻塞渲染,那么 CSS 文件下載之前,瀏覽器就會渲染出一個紅色的 div ,之后再變成藍色。瀏覽器的這個策略其實很明智的,想象一下,如果沒有這個策略,頁面首先會呈現出一個原始的模樣,待 CSS 下載完之后又突然變了一個模樣。用戶體驗可謂極差,而且渲染是有成本的。
因此,基于性能與用戶體驗這兩點的考慮,瀏覽器會盡量減少渲染的次數, CSS 順理成章地阻塞頁面渲染。
現在,將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>
會有如下情況發生:
會發現,css文件在js文件之前時,css和js文件雖然都下載了,但是js的執行被阻塞了(網上很多blog說這里css阻塞了js的加載是不對的,應該是阻塞了js的執行),導致DOM tree的構建被阻塞了。
這里,我們稍作修改,給script加上defer
<script defer src="./test.js"></script>
這里,由于script延遲執行,所以就不會阻塞DOM tree的構建了。
所以,我們總結一下:
- css如果在js之前,會阻塞js的執行,從而阻塞DOM tree構建
- 要想不阻塞DOM tree構建,需要將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的加載,這是由于瀏覽器的預解析優化,會新開一個線程預加載后續資源。但是開始在頁面只有一個DIV,說明DOM tree構建確實被阻塞了。而且在test.jss執行過程中,瀏覽器已經將渲染好的一個紅色div呈現給了用戶。
因為瀏覽器不知道腳本的內容,因而碰到腳本時,只好先渲染頁面,確保腳本能獲取到最新的DOM元素信息,盡管腳本可能不需要這些信息。
9.3 阻塞總結
我們分析如上幾種情況,總結如下:
html解析的過程中遇到script時,如果是嵌入腳本,會執行并阻塞dom tree構建;如果是外鏈JS腳本,則會進行加載后執行,并阻塞dom tree構建。但不管怎樣,由于瀏覽器的預解析優化,會新開一個線程加載后續資源。并且,為了確保js能拿到最新的DOM元素信息 CSSOM信息,js執行前會等待css加載完畢并渲染頁面。
10. 總結
看到這里,想必大家對瀏覽器加載解析渲染機制已經有了比較清晰的認識。下一篇,我們將對照這篇文章分析一下這個過程中可以幫助提高性能的優化點。
參考
http://taligarsiel.com/Projects/howbrowserswork1.htm
[譯]How browsers work
了解html頁面的渲染過程
瀏覽器加載網頁時的過程是什么?-creative web developer回答
瀏覽器加載、解析、渲染的過程
漲知識!原來CSS與JS是這樣阻塞DOM解析和渲染的
補充:
http://www.lxweimin.com/p/e4a75cb6f268
https://www.cnblogs.com/echo-hui/p/9231031.html