本文示例源代碼請戳 github博客,建議你們動手敲敲代碼。
瀏覽器渲染頁面的過程css
從耗時的角度,瀏覽器請求、加載、渲染一個頁面,時間花在下面五件事情上:html
本文討論第五個部分,即瀏覽器對內容的渲染,這一部分(渲染樹構建、佈局及繪製),又能夠分爲下面五個步驟:node
須要明白,這五個步驟並不必定一次性順序完成。若是 DOM 或 CSSOM 被修改,以上過程須要重複執行,這樣才能計算出哪些像素須要在屏幕上進行從新渲染。實際頁面中,CSS 與 JavaScript 每每會屢次修改 DOM 和 CSSOM。git
在詳細說明以前咱們來看一下瀏覽器線程。這將有助於咱們理解後續內容。github
瀏覽器是多線程的,它們在內核制控下相互配合以保持同步。一個瀏覽器至少實現三個常駐線程:JavaScript 引擎線程,GUI 渲染線程,瀏覽器事件觸發線程。web
瀏覽器從網絡或硬盤中得到HTML字節數據後會通過一個流程將字節解析爲DOM樹:ajax
<html>、<body>
這樣的標籤以及標籤中的字符串和屬性等都會被轉化爲令牌,每一個令牌具備特殊含義和一組規則)。令牌記錄了標籤的開始與結束,經過這個特性能夠輕鬆判斷一個標籤是否爲子標籤(假設有<html>
與<body>
兩個標籤,當<html>
標籤的令牌還未遇到它的結束令牌</html>
就碰見了<body>
標籤令牌,那麼<body>
就是<html>
的子標籤)。整個DOM樹的構建過程其實就是: 字節 -> 字符 -> 令牌 -> 節點對象 -> 對象模型,
下面將經過一個示例HTML代碼與配圖更形象地解釋這個過程。chrome
<html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <link href="style.css" rel="stylesheet"> <title>Critical Path</title> </head> <body> <p>Hello <span>web performance</span> students!</p> <div><img src="awesome-photo.jpg"></div> </body> </html>
當上述HTML代碼碰見<link>標籤時,瀏覽器會發送請求得到該標籤中標記的CSS文件(使用內聯CSS能夠省略請求的步驟提升速度,但沒有必要爲了這點速度而丟失了模塊化與可維護性),style.css中的內容以下:segmentfault
body { font-size: 16px } p { font-weight: bold } span { color: red } p span { display: none } img { float: right }
瀏覽器得到外部CSS文件的數據後,就會像構建DOM樹同樣開始構建CSSOM樹,這個過程沒有什麼特別的差異。
瀏覽器
在構建了DOM樹和CSSOM樹以後,瀏覽器只是擁有了兩個互相獨立的對象集合,DOM樹描述了文檔的結構與內容,CSSOM樹則描述了對文檔應用的樣式規則,想要渲染出頁面,就須要將DOM樹與CSSOM樹結合在一塊兒,這就是渲染樹。
CSS採用了一種叫作盒子模型的思惟模型來表示每一個節點與其餘元素之間的距離,盒子模型包括外邊距(Margin),內邊距(Padding),邊框(Border),內容(Content)。頁面中的每一個標籤其實都是一個個盒子
佈局階段會從渲染樹的根節點開始遍歷,而後肯定每一個節點對象在頁面上的確切大小與位置,佈局階段的輸出是一個盒子模型,它會精確地捕獲每一個元素在屏幕內的確切位置與大小,全部相對的測量值也都會被轉換爲屏幕內的絕對像素值。
<html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>Critial Path: Hello world!</title> </head> <body> <div style="width: 50%"> <div style="width: 50%">Hello world!</div> </div> </body> </html>
當Layout佈局事件完成後,瀏覽器會當即發出Paint Setup與Paint事件,開始將渲染樹繪製成像素,繪製所需的時間跟CSS樣式的複雜度成正比,繪製完成後,用戶就能夠看到頁面的最終呈現效果了。
咱們對一個網頁發送請求並得到渲染後的頁面可能也就通過了1~2秒,但瀏覽器其實已經作了上述所講的很是多的工做,總結一下瀏覽器關鍵渲染路徑的整個過程:
爲了直觀的觀察瀏覽器加載和渲染的細節,本地用nodejs搭建一個簡單的HTTP Server。
index.js
const http = require('http'); const fs = require('fs'); const hostname = '127.0.0.1'; const port = 8080; http.createServer((req, res) => { if (req.url == '/a.js') { fs.readFile('a.js', 'utf-8', function (err, data) { res.writeHead(200, {'Content-Type': 'text/plain'}); setTimeout(function () { res.write(data); res.end() }, 5000) }) } else if (req.url == '/b.js') { fs.readFile('b.js', 'utf-8', function (err, data) { res.writeHead(200, {'Content-Type': 'text/plain'}); res.write(data); res.end() }) } else if (req.url == '/style.css') { fs.readFile('style.css', 'utf-8', function (err, data) { res.writeHead(200, {'Content-Type': 'text/css'}); res.write(data); res.end() }) } else if (req.url == '/index.html') { fs.readFile('index.html', 'utf-8', function (err, data) { res.writeHead(200, {'Content-Type': 'text/html'}); res.write(data); res.end() }) } }).listen(port, hostname, () => { console.log('Server running at ' + hostname + ':' + port); });
index.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="cache-control" content="no-cache,no-store, must-revalidate"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>瀏覽器渲染</title> <link rel="stylesheet" href="http://127.0.0.1:8080/style.css"> <script src='http://127.0.0.1:8080/a.js'></script> </head> <body> <p id='header'>1111111</p> <script src='http://127.0.0.1:8080/b.js'></script> <p>222222</p> <p>3333333</p> </body> </html>
style.css
#header{ color: red; }
a.js、b.js
暫時爲空
能夠看到,服務端將對a.js的請求延遲5秒返回。Server啓動後,在chrome瀏覽器中打開http://127.0.0.1:8080/index.html
咱們打開chrome的調試面板
第一次解析html的時候,外部資源好像是一塊兒請求的,說資源是預解析加載的,就是說style.css和b.js是a.js形成阻塞的時候才發起的請求,圖中也是能夠解釋得通,由於第一次Parse HTML的時候就遇到阻塞,而後預解析就去發起請求,因此看起來是一塊兒請求的。
咱們修改一下html代碼
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="cache-control" content="no-cache,no-store, must-revalidate"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>瀏覽器渲染</title> <link rel="stylesheet" href="http://127.0.0.1:8080/style.css"> </head> <body> <p id='header'>1111111</p> <script src='http://127.0.0.1:8080/a.js'></script> <script src='http://127.0.0.1:8080/b.js'></script> <p>222222</p> <p>3333333</p> </body> </html>
由於a.js的延遲,解析到a.js所在的script標籤的時候,a.js尚未下載完成,阻塞並中止解析,以前解析的已經繪製顯示出來了。當a.js下載完成並執行完以後繼續後面的解析。固然,瀏覽器不是解析一個標籤就繪製顯示一次,當遇到阻塞或者比較耗時的操做的時候纔會先繪製一部分解析好的。
修改index.html:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="cache-control" content="no-cache,no-store, must-revalidate"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>瀏覽器渲染</title> <link rel="stylesheet" href="http://127.0.0.1:8080/style.css"> <script src='http://127.0.0.1:8080/a.js'></script> <script src='http://127.0.0.1:8080/b.js'></script> </head> <body> <p id='header'>1111111</p> <p>222222</p> <p>3333333</p> </body> </html>
由於a.js的阻塞使得解析中止,a.js下載完成以前,頁面沒法顯示任何東西。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="cache-control" content="no-cache,no-store, must-revalidate"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>瀏覽器渲染</title> <link rel="stylesheet" href="http://127.0.0.1:8080/style.css"> </head> <body> <p id='header'>1111111</p> <script src='http://127.0.0.1:8080/a.js'></script> <script src='http://127.0.0.1:8080/b.js'></script> <p>222222</p> <p>3333333</p> </body> </html>
解析到js文件時出現阻塞。阻塞後面的解析,致使後面的不能很快的顯示。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="cache-control" content="no-cache,no-store, must-revalidate"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>瀏覽器渲染</title> <link rel="stylesheet" href="http://127.0.0.1:8080/style.css"> </head> <body> <p id='header'>1111111</p> <p>222222</p> <p>3333333</p> <script src='http://127.0.0.1:8080/a.js'></script> <script src='http://127.0.0.1:8080/b.js'></script> </body> </html>
解析到a.js部分的時候,頁面要顯示的東西已經解析完了,a.js不會影響頁面的呈現速度。
由上面咱們能夠總結一下
下面咱們來看下異步js
接下來咱們對比下 defer 和 async 屬性的區別:
其中藍色線表明JavaScript加載;紅色線表明JavaScript執行;綠色線表明 HTML 解析。
<script src="script.js"></script>
沒有 defer 或 async,瀏覽器會當即加載並執行指定的腳本,也就是說不等待後續載入的文檔元素,讀到就加載並執行。
async 屬性表示異步執行引入的 JavaScript,與 defer 的區別在於,若是已經加載好,就會開始執行——不管此刻是 HTML 解析階段仍是 DOMContentLoaded 觸發以後。須要注意的是,這種方式加載的 JavaScript 依然會阻塞 load 事件。換句話說,async-script 可能在 DOMContentLoaded 觸發以前或以後執行,但必定在 load 觸發以前執行。
defer 屬性表示延遲執行引入的 JavaScript,即這段 JavaScript 加載時 HTML 並未中止解析,這兩個過程是並行的。整個 document 解析完畢且 defer-script 也加載完成以後(這兩件事情的順序無關),會執行全部由 defer-script 加載的 JavaScript 代碼,而後觸發 DOMContentLoaded 事件。
defer 與相比普通 script,有兩點區別:
服務端將style.css
的相應也設置延遲。
fs.readFile('style.css', 'utf-8', function (err, data) { res.writeHead(200, {'Content-Type': 'text/css'}); setTimeout(function () { res.write(data); res.end() }, 5000) })
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="cache-control" content="no-cache,no-store, must-revalidate"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>瀏覽器渲染</title> <link rel="stylesheet" href="http://127.0.0.1:8080/style.css"> </head> <body> <p id='header'>1111111</p> <p>222222</p> <p>3333333</p> <script src='http://127.0.0.1:8080/a.js' async></script> <script src='http://127.0.0.1:8080/b.js' async></script> </body> </html>
能夠看出來,css文件不會阻塞HTML解析,可是會阻塞渲染,致使css文件未下載完成以前已經解析好html也沒法先顯示出來。
咱們把css調整到尾部
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="cache-control" content="no-cache,no-store, must-revalidate"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>瀏覽器渲染</title> </head> <body> <p id='header'>1111111</p> <p>222222</p> <p>3333333</p> <link rel="stylesheet" href="http://127.0.0.1:8080/style.css"> <script src='http://127.0.0.1:8080/a.js' async></script> <script src='http://127.0.0.1:8080/b.js' async></script> </body> </html>
這是頁面能夠渲染了,可是沒有樣式。直到css加載完成
以上咱們能夠簡單總結。