通過chrome偵錯程式測試瞭解瀏覽器解析和渲染HTML的過程
1.基礎知識:瞭解chrome的Timeline工具
僅僅是通過理論知識,很難記住和理解瀏覽器解析html的原則,因此我動手做了些小實驗。而做這個實驗,不得不用到一個工具:chrome的Timeline工具。
這個工具真的很強大,Timeline工具欄提供了對於在裝載Web應用的過程中,時間花費情況的概覽,這些應用包括處理DOM事件, 頁面佈局渲染或者向螢幕繪製元素。Timeline可以通過事件,框架,和實時記憶體用量3個方面的資料來監測網頁,通過這些資料,我們可以方便的找出頁面中存在問題的地方。
具體的使用方式,這篇部落格裡有詳盡的說明。
2.主要過程
- 解析HTML
- 構建DOM樹
- DOM樹與CSS樣式進行附著構造呈現樹
- 佈局
- 繪製
3.解析與構建DOM樹
瀏覽器有專門的html解析器,並且是邊解析邊構建do’m樹的,因此將前兩部分放在一塊講。
總體的解析原則是:1.自上而下順序解析。2.解析過程中遇到外部樣式(link,style)和外部指令碼(script),會阻塞瀏覽器的解析。3.外部樣式和外部指令碼(在沒有async、defer屬性下)會並行載入,但是外部樣式會阻塞外部指令碼的執行。
即:html解析->外部樣式、指令碼載入->外部樣式執行->外部指令碼執行->html繼續解析
情況一:如果是動態指令碼(即內聯指令碼)則不受樣式影響,在解析到它時會執行。
情況二:如果是動態建立的樣式檔案,則不會阻塞後續任何型別指令碼的執行。
情況三:外部樣式後續
3.1外部樣式、指令碼並行載入,外部樣式會阻塞後續指令碼執行,直到外部樣式載入並解析完畢。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JS Bin</title>
<script>var start = +new Date;</script>
<link href="http://udacity-crp.herokuapp.com/style.css?rtt=2" rel="stylesheet">
<link href="http://udacity-crp.herokuapp.com/style.css?rtt=1" rel="stylesheet">
</head>
<body>
<p>I am here!</p>
<span id="result"></span>
<p>I am here two!</p>
<script>
var end = +new Date;
document.getElementById('result').innerHTML = (end-start);
</script>
</body>
</html>
程式碼執行結果:
並行載入:
阻塞執行:
可以看到,外部css載入並解析(parse)完畢之後才執行後面的js程式碼。
注意
dom樹構建完畢會觸發DOMContentLoaded事件,但此時外部檔案不一定載入完畢,比如一些圖片。當頁面載入完畢後才出發load事件,這也是 jquery的$ (document).ready(function(){})和原生程式碼window.onload=function(){}的區別。(其他區別: window.onload不能同時編寫多個,如果有多個window.onload方法,只會執行一個; $(document).ready()可以同時編寫多個,並且都可以得到執行 )
3.2 外部樣式不會阻塞後續指令碼的載入,但會阻塞後續指令碼的執行
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JS Bin</title>
<script>var start = +new Date;</script>
<link href="http://udacity-crp.herokuapp.com/style.css?rtt=2" rel="stylesheet">
</head>
<body>
test
<script src="http://udacity-crp.herokuapp.com/time.js?rtt=1&a"></script>
<div id="result"></div>
<script>var end = +new Date;document.getElementById("result").innerHTML = end-start;</script>
</body>
</html>
外部指令碼程式碼:
var loadTime = document.createElement('div');
loadTime.innerText = document.currentScript.src + ' executed @ ' + window.performance.now();
loadTime.style.color = 'blue';
document.body.appendChild(loadTime);
執行結果:
在瀑布流中檢視外部檔案載入順序:
可以看到,確實是按先後順序發起請求,但是並行載入。
可以看你到,在外部樣式載入和解析完畢,外部指令碼才開始執行
3.3 如果後續外部指令碼含有async屬性,則該指令碼不會被樣式檔案阻塞
這裡我直接在3.2 中外部指令碼標籤中加入async ,程式碼就不貼了,看下執行結果:
可以看到 外部引入的time.js檔案在不沒有被css阻塞。
3.4 動態建立的樣式檔案不會阻塞其後的script的執行,不管script標籤是否具有async屬性
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JS Bin</title>
<script>var start = +new Date;</script>
</head>
<body>
test
<script>
var link = document.createElement('link');
link.href = "http://udacity-crp.herokuapp.com/style.css?rtt=2";
link.rel = "stylesheet";
document.head.appendChild(link);
</script>
<div id="result"></div>
<script>var end = +new Date;document.getElementById("result").innerHTML = end-start;</script>
</body>
</html>
執行結果:
動態建立的link載入了3秒左右,而第一個script指令碼(line:6)在67ms時執行,第三個script指令碼(line:19)在75ms左右執行。
3.5動態建立的樣式檔案不會阻塞其後動態建立的script的執行
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JS Bin</title>
<script>var start = +new Date;</script>
</head>
<body>
test
<script>
var link = document.createElement('link');
link.href = "http://udacity-crp.herokuapp.com/style.css?rtt=2";
link.rel = "stylesheet";
document.head.appendChild(link);
var script = document.createElement('script');
script.src = "http://udacity-crp.herokuapp.com/time.js?rtt=1&a";
document.head.appendChild(script);
</script>
<div id="result"></div>
<script>var end = +new Date;document.getElementById("result").innerHTML = end-start;</script>
</body>
</html>
可以看到,動態指令碼在3秒左右才載入完畢,後其後的指令碼在1.8秒左右就以及載入並執行了。另外第三個script標籤在2ms左右就執行了,可見,js指令碼無論是動態建立還是直接引入在頁面的,順序載入,但並行執行。這個在3.6節進行進一步證明。
3.6動態建立的指令碼的載入和執行均會被之前的樣式檔案阻塞,但不會阻塞後續指令碼的執行
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JS Bin</title>
<script>var start = +new Date;</script>
<link href="http://udacity-crp.herokuapp.com/style.css?rtt=2" rel="stylesheet">
</head>
<body>
test
<script>
var script = document.createElement('script');
script.src = "http://udacity-crp.herokuapp.com/time.js?rtt=1&a";
document.head.appendChild(script);
</script>
</script>
<div id="result"></div>
<script>var end = +new Date;document.getElementById("result").innerHTML = end-start;</script>
</body>
</html>
可以看到,動態建立的指令碼並沒有阻塞後面指令碼的執行,而且它的執行方式和其他指令碼一樣,均會被之前的CSS阻塞
4 構建渲染樹,繪製與佈局
在外部樣式執行完畢後,css附著於DOM,建立了一個渲染樹(渲染樹是一些被渲染物件的集),也就是說渲染樹與dom樹是同時構建的。每個渲染物件都包含了與之對應的計算過樣式的DOM物件,對於每個渲染元素來說,位置都經過計算,所以這裡被叫做“佈局”。然後將“佈局”顯示在瀏覽器視窗,稱之為“繪製”。
同指令碼檔案一樣,CSS樣式表會阻塞圖片的載入,而指令碼檔案不會。接著指令碼的執行完畢後,DOM樹構建完成。
渲染樹的節點(渲染器),在Gecko中稱為frame,而在webkit中稱為renderer。渲染器是在文件解析和建立DOM節點後建立的,會計算DOM節點的樣式資訊。
在webkit中,renderer是由DOM節點呼叫attach()方法建立的。attach()方法計算了DOM節點的樣式資訊。attach()是自上而下的遞迴操作。也就是說,父節點總是比子節點先建立自己的renderer。銷燬的時候,則是自下而上的遞迴操作,也就是說,子節點總是比父節點先銷燬。
如果元素的display屬性被設定成了none,或者如果元素的子孫繼承了display:none,renderer不會被建立。節點的子類和display屬性一起決定為該節點建立什麼樣的渲染器。但是visibility:hidden的元素會被建立
具體如何構建渲染樹,可參見: