本文中瀏覽器特指Chrome瀏覽器
開始以前說說幾個概念,以及在準備寫這篇文章以前對瀏覽器的渲染機制的瞭解:javascript
DOM:Document Object Model,瀏覽器將HTML解析成樹形的數據結構,簡稱DOM。
CSSOM:CSS Object Model,瀏覽器將CSS代碼解析成樹形的數據結構
Render Tree:DOM 和 CSSOM 合併後生成 Render Tree(Render Tree 和DOM同樣,以多叉樹的形式保存了每一個節點的css屬性、節點自己屬性、以及節點的孩子節點,display:none 的節點不會被加入 Render Tree,而 visibility: hidden 則會,因此,若是某個節點最開始是不顯示的,設爲 display:none 是更優的。)
查閱了一些關於瀏覽器渲染機制的文章後,獲得如下比較重要或者有爭議性的觀點:css
1. Create/Update DOM And request css/image/js:瀏覽器請求到HTML代碼後,在生成DOM的最開始階段(應該是 Bytes → characters 後),並行發起css、圖片、js的請求,不管他們是否在HEAD裏。 注意:發起 js 文件的下載 request 並不須要 DOM 處理到那個 script 節點,好比:簡單的正則匹配就能作到這一點,雖然實際上並不必定是經過正則:)。這是不少人在理解渲染機制的時候存在的誤區。
2. Create/Update Render CSSOM:CSS文件下載完成,開始構建CSSOM
3. Create/Update Render Tree:全部CSS文件下載完成,CSSOM構建結束後,和 DOM 一塊兒生成 Render Tree。
4. Layout:有了Render Tree,瀏覽器已經能知道網頁中有哪些節點、各個節點的CSS定義以及他們的從屬關係。下一步操做稱之爲Layout,顧名思義就是計算出每一個節點在屏幕中的位置。
5. Painting:Layout後,瀏覽器已經知道了哪些節點要顯示(which nodes are visible)、每一個節點的CSS屬性是什麼(their computed styles)、每一個節點在屏幕中的位置是哪裏(geometry)。就進入了最後一步:Painting,按照算出來的規則,經過顯卡,把內容畫到屏幕上。
出處html
瀏覽器的主要組件爲 (1.1):
1. 用戶界面 - 包括地址欄、前進/後退按鈕、書籤菜單等。除了瀏覽器主窗口顯示的您請求的頁面外,其餘顯示的各個部分都屬於用戶界面。
2. 瀏覽器引擎 - 在用戶界面和呈現引擎之間傳送指令。
3. 呈現引擎 - 負責顯示請求的內容。若是請求的內容是 HTML,它就負責解析 HTML 和 CSS 內容,並將解析後的內容顯示在屏幕上。
4. 網絡 - 用於網絡調用,好比 HTTP 請求。其接口與平臺無關,併爲全部平臺提供底層實現。
5. 用戶界面後端 - 用於繪製基本的窗口小部件,好比組合框和窗口。其公開了與平臺無關的通用接口,而在底層使用操做系統的用戶界面方法。
6. JavaScript 解釋器。用於解析和執行 JavaScript 代碼。
7. 數據存儲。這是持久層。瀏覽器須要在硬盤上保存各類數據,例如 Cookie。新的 HTML 規範 (HTML5) 定義了「網絡數據庫」,這是一個完整(可是輕便)的瀏覽器內數據庫。
值得注意的是,和大多數瀏覽器不一樣,Chrome 瀏覽器的每一個標籤頁都分別對應一個呈現引擎實例。每一個標籤頁都是一個獨立的進程。主流程
呈現引擎一開始會從網絡層獲取請求文檔的內容,內容的大小通常限制在 8000 個塊之內。
而後進行以下所示的基本流程:
呈現引擎將開始解析 HTML 文檔,並將各標記逐個轉化成「內容樹」上的 DOM 節點。同時也會解析外部 CSS 文件以及樣式元素中的樣式數據。HTML 中這些帶有視覺指令的樣式信息將用於建立另外一個樹結構:呈現樹。
呈現樹包含多個帶有視覺屬性(如顏色和尺寸)的矩形。這些矩形的排列順序就是它們將在屏幕上顯示的順序。
呈現樹構建完畢以後,進入「佈局」處理階段,也就是爲每一個節點分配一個應出如今屏幕上的確切座標。下一個階段是繪製 - 呈現引擎會遍歷呈現樹,由用戶界面後端層將每一個節點繪製出來。
須要着重指出的是,這是一個漸進的過程。爲達到更好的用戶體驗,呈現引擎會力求儘快將內容顯示在屏幕上。它沒必要等到整個 HTML 文檔解析完畢以後,就會開始構建呈現樹和設置佈局。在不斷接收和處理來自網絡的其他內容的同時,呈現引擎會將部份內容解析並顯示出來。html5解析算法
HTML 沒法用常規的自上而下或自下而上的解析器進行解析。
緣由在於:
1.語言的寬容本質。
2.瀏覽器從來對一些常見的無效 HTML 用法採起包容態度。
3.解析過程須要不斷地反覆。源內容在解析過程當中一般不會改變,可是在 HTML 中,腳本標記若是包含 document.write,就會添加額外的標記,這樣解析過程實際上就更改了輸入內容。
因爲不能使用常規的解析技術,瀏覽器就建立了自定義的解析器來解析 HTMLjava處理腳本和樣式表的順序
腳本
網絡的模型是同步的。網頁做者但願解析器遇到 <script> 標記時當即解析並執行腳本。文檔的解析將中止,直到腳本執行完畢。若是腳本是外部的,那麼解析過程會中止,直到從網絡同步抓取資源完成後再繼續。此模型已經使用了多年,也在 HTML4 和 HTML5 規範中進行了指定。做者也能夠將腳本標註爲「defer」,這樣它就不會中止文檔解析,而是等到解析結束才執行。HTML5 增長了一個選項,可將腳本標記爲異步,以便由其餘線程解析和執行。
預解析
WebKit 和 Firefox 都進行了這項優化。在執行腳本時,其餘線程會解析文檔的其他部分,找出並加載須要經過網絡加載的其餘資源。經過這種方式,資源能夠在並行鏈接上加載,從而提升整體速度。請注意,預解析器不會修改 DOM 樹,而是將這項工做交由主解析器處理;預解析器只會解析外部資源(例如外部腳本、樣式表和圖片)的引用。
樣式表
另外一方面,樣式表有着不一樣的模型。理論上來講,應用樣式表不會更改 DOM 樹,所以彷佛沒有必要等待樣式表並中止文檔解析。但這涉及到一個問題,就是腳本在文檔解析階段會請求樣式信息。若是當時尚未加載和解析樣式,腳本就會得到錯誤的回覆,這樣顯然會產生不少問題。這看上去是一個非典型案例,但事實上很是廣泛。Firefox 在樣式表加載和解析的過程當中,會禁止全部腳本。而對於 WebKit 而言,僅當腳本嘗試訪問的樣式屬性可能受還沒有加載的樣式表影響時,它纔會禁止該腳本。
呈現樹構建
在 DOM 樹構建的同時,瀏覽器還會構建另外一個樹結構:呈現樹。這是由可視化元素按照其顯示順序而組成的樹,也是文檔的可視化表示。它的做用是讓您按照正確的順序繪製內容。node
出處web
根據以上長篇大論,能夠歸結爲如下幾點:算法
文章一:
1.瀏覽器請求到html結構後,併發請求js,css,圖片等資源,並非解析到相應節點纔去發送網絡請求。文章二:
1.HTML解析爲dom樹,不是簡單的自上而下,而是須要不斷地反覆,好比解析到腳本標籤,腳本修改以前已經解析的dom,這就要往回從新解析一遍
2.HTML 解析一部分就顯示一部分(無論樣式表是否已經下載完成)
3.<script> 標記會阻塞文檔的解析(DOM樹的構建)直到腳本執行完,若是腳本是外部的,需等到腳本下載並執行完成才繼續往下解析。
4.外部資源是解析過程當中預解析加載的(腳本阻塞瞭解析,其餘線程會解析文檔的其他部分,找出並加載),而不是一開始就一塊兒請求的(實際上看起來也是併發請求的,由於請求不相互依賴)chrome
爲了直觀的觀察瀏覽器加載和渲染的細節,本地用nodejs搭建一個簡單的HTTP Server。
server.js:shell
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() }, 10000) }) } 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); });
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> <script src='http://127.0.0.1:8080/a.js'></script> <link rel="stylesheet" href="http://127.0.0.1:8080/style.css"> </head> <body> <p id='hh'>1111111</p> <script src='http://127.0.0.1:8080/b.js'></script> <p>222222</p> <p>3333333</p> </body> </html>
能夠看到,服務端將對a.js的請求延遲10秒返回。
Server啓動後,在chrome瀏覽器中打開http://127.0.0.1:8080/index.html
看一下TimeLine
能夠看到,第一次解析html的時候,外部資源好像是一塊兒請求的,最後一次Finish Loading是a.js的,由於服務端延遲的10秒鐘。文章二中說資源是預解析加載的,就是說style.css和b.js是a.js形成阻塞的時候才發起的請求,圖中也是能夠解釋得通,由於第一次Parse HTML的時候就遇到阻塞,而後預解析就去發起請求,因此看起來是一塊兒請求的。
將index.html內容增長足夠多,而且在最後面才加入script:
<!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='hh'>1111111</p> <p>重複</p> <p>重複</p> .... ....重複5000行 <script src='http://127.0.0.1:8080/b.js'></script> <script src='http://127.0.0.1:8080/a.js'></script> <p>3333333</p> </body> </html>
多刷新幾回,查看TimeLine
能夠發現,當html內容太多的時候,瀏覽器須要分段接收,解析的時候也要分段解析。還能夠看到,請求資源的時機是沒法肯定的,但確定不是同時請求的,也不是解析到指定標籤的時候纔去請求,瀏覽器會自行判斷,若是當前操做比較耗時,就會去加載後面的資源。
修改 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"> </head> <body> <p id='hh'>1111111</p> <p>222222</p> <script src='http://127.0.0.1:8080/b.js'></script> <script src='http://127.0.0.1:8080/a.js'></script> <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/b.js'></script> <script src='http://127.0.0.1:8080/a.js'></script> </head> <body> <p id='hh'>1111111</p> <p>222222</p> <p>3333333</p> </body> </html>
仍是由於a.js的阻塞使得解析中止,a.js下載完成以前,頁面沒法顯示任何東西。
整個處理過程當中,Parse HTML 3次,計算元素樣式1次,頁面佈局計算1次,繪製一次。
修改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"> </head> <body> <p id='hh'>1111111</p> <p>222222</p> <p>3333333</p> <script src='http://127.0.0.1:8080/b.js'></script> <script src='http://127.0.0.1:8080/a.js'></script> </body> </html>
解析到a.js部分的時候,頁面要顯示的東西已經解析完了,a.js不會影響頁面的呈現速度。
整個處理過程當中,Parse HTML 3次,計算元素樣式2次,頁面佈局計算1次,繪製一次。
修改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"> </head> <body> <p id='hh'>1111111</p> <script src='http://127.0.0.1:8080/b.js'></script> <script src='http://127.0.0.1:8080/a.js'></script> <p>222222</p> <p>3333333</p> </body> </html>
阻塞後面的解析,致使不能很快的顯示。
整個處理過程當中,Parse HTML 3次,計算元素樣式2次,頁面佈局計算2次,繪製2次。
能夠發現瀏覽器優化得很是好,當阻塞在a.js的時候,現將已經解析的部分顯示(計算元素樣式,佈局排版,繪製),當a.js下載好後接着解析和顯示後面的(由於a.js後面還有要顯示到頁面上的元素,因此還須要進行1次計算元素樣式,佈局排版,繪製)
修改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"> </head> <body> <p id='hh'>1111111</p> <p>222222</p> <script src='http://127.0.0.1:8080/a.js'></script> <p>3333333</p> <script> document.getElementById("hh").style.height="200px"; </script> </body> </html>
a.js阻塞的時候,排版,繪製1次;a.js下載完後重排,重繪一次;修改DOM,引發重排,重繪一次。是否是這樣呢?看下圖
事實是修改DOM並無引發重排,重繪。由於瀏覽器將a.js下載完成並執行後的一次重排和重繪與修改DOM本應該致使的重排和重繪積攢一批,而後作一次重排,重繪
瀏覽器是聰明的,它不會你每改一次樣式,它就reflow或repaint一次。 通常來講,瀏覽器會把這樣的操做積攢一批,而後作一次reflow,這又叫異步reflow或增量異步reflow。可是有些狀況瀏覽器是不會這麼作的,好比:resize窗口,改變了頁面默認的字體,等。對於這些操做,瀏覽器會立刻進行reflow。
服務端將style.css的相應也設置延遲。
修改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"> </head> <body> <p id='hh'>1111111</p> <p>222222</p> <p>3333333</p> <script src='http://127.0.0.1:8080/a.js'></script> </body> </html>
能夠看出來,css文件不會阻塞HTML解析,可是會阻塞渲染,致使css文件未下載完成以前已經解析好html也沒法先顯示出來。
接着修改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> </head> <body> <p id='hh'>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'></script> </body> </html>
不會阻塞渲染,引發頁面抖動
修改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" media="print"> </head> <body> <p id='hh'>1111111</p> <p>222222</p> <p>3333333</p> <script src='http://127.0.0.1:8080/a.js'></script> </body> </html>
注意media="print"
由於指定了media="print",樣式不起做用,不會阻塞渲染。
<link href="style.css" rel="stylesheet">
<link href="style.css" rel="stylesheet" media="all">
<link href="portrait.css" rel="stylesheet media="orientation:portrait">
<link href="print.css" rel="stylesheet" media="print">
第一條聲明阻塞渲染,匹配全部狀況。 第二條聲明同樣阻塞渲染:"all" 是默認類型,若是你未指定任何類型,則默認爲 "all"。所以,第一條聲明和第二條聲明其實是同樣的。 第三條聲明有一條動態媒體查詢,在頁面加載時判斷。根據頁面加載時設備的方向,portrait.css 可能阻塞渲染,也可能不阻塞。 最後一條聲明只適用打印,所以,頁面在瀏覽器中首次加載時,不會阻塞渲染。
可是。。。看一下火狐的表現
修改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" media="print"> </head> <body> <p id='hh'>1111111</p> <p>222222</p> <img src="emmet.png"> <p>3333333</p> </body> </html>
圖片比較大,2M多,但服務端仍是要延遲10秒響應。
圖片既不阻塞解析,也不阻塞渲染。
圖片未請求回來以前,先進行一次layout和paint,paint的範圍就是頁面初始的可視區域。當返回一部分圖片信息後(估計是獲得了圖片的尺寸),再進行一次layout和paint,paint的範圍受到圖片尺寸的影響。當圖片信息所有返回時,最後進行一次paint。
若是固定img的寬高,當返回一部分圖片信息後,不會再layout,但仍會paint一次。
補充:圖片用做背景(不是寫在CSS文件內)是在Recalculate Style的時候才發起的請求,layout、paint次數和固定寬高的img同樣。背景圖屬性寫在CSS文件裏,則CSS文件下載並執行Recalculate Style的時候纔會請求圖片。
參考
瀏覽器的渲染原理簡介
瀏覽器的工做原理:新式網絡瀏覽器幕後揭祕
JS 必定要放在 Body 的最底部麼?聊聊瀏覽器的渲染機制
https://blog.chromium.org/2015/03/new-javascript-techniques-for-rapid.html
https://developers.google.cn/web/fundamentals/performance/critical-rendering-path/render-blocking-css