你們都知道萬維網的應用層使用了HTTP
協議,而且用瀏覽器做爲入口訪問網絡上的資源。用戶在使用瀏覽器訪問一個網站時須要先經過HTTP
協議向服務器發送請求,以後服務器返回HTML
文件與響應信息。這時,瀏覽器會根據HTML
文件來進行解析與渲染(該階段還包括向服務器請求非內聯的CSS
文件與JavaScript
文件或者其餘資源),最終再將頁面呈如今用戶面前。javascript
如今知道了網頁的渲染都是由瀏覽器完成的,那麼若是一個網站的頁面加載速度太慢會致使用戶體驗不夠友好,本文經過詳解瀏覽器渲染頁面的過程來引入一些基本的瀏覽器性能優化方案。讓瀏覽器更快地渲染你的網頁並快速響應從而提升用戶體驗。css
本文做者爲: SylvanasSun(sylvanas.sun@gmail.com).轉載請務必將下面這段話置於文章開頭處(保留超連接).
本文首發自SylvanasSun Blog,原文連接: sylvanassun.github.io/2017/10/03/…html
瀏覽器接收到服務器返回的HTML
、CSS
和JavaScript
字節數據並對其進行解析和轉變成像素的渲染過程被稱爲關鍵渲染路徑。經過優化關鍵渲染路徑便可以縮短瀏覽器渲染頁面的時間。前端
瀏覽器在渲染頁面前須要先構建出DOM
樹與CSSOM
樹(若是沒有DOM
樹和CSSOM
樹就沒法肯定頁面的結構與樣式,因此這兩項是必須先構建出來的)。java
DOM
樹全稱爲Document Object Model
文檔對象模型,它是HTML
和XML
文檔的編程接口,提供了對文檔的結構化表示,並定義了一種可使程序對該結構進行訪問的方式(好比JavaScript
就是經過DOM
來操做結構、樣式和內容)。DOM
將文檔解析爲一個由節點和對象組成的集合,能夠說一個WEB
頁面其實就是一個DOM
。git
CSSOM
樹全稱爲Cascading Style Sheets Object Model
層疊樣式表對象模型,它與DOM
樹的含義相差不大,只不過它是CSS
的對象集合。程序員
瀏覽器從網絡或硬盤中得到HTML
字節數據後會通過一個流程將字節解析爲DOM
樹:github
編碼: 先將HTML
的原始字節數據轉換爲文件指定編碼的字符。web
令牌化: 而後瀏覽器會根據HTML
規範來將字符串轉換成各類令牌(如<html>
、<body>
這樣的標籤以及標籤中的字符串和屬性等都會被轉化爲令牌,每一個令牌具備特殊含義和一組規則)。令牌記錄了標籤的開始與結束,經過這個特性能夠輕鬆判斷一個標籤是否爲子標籤(假設有<html>
與<body>
兩個標籤,當<html>
標籤的令牌還未遇到它的結束令牌</html>
就碰見了<body>
標籤令牌,那麼<body>
就是<html>
的子標籤)。算法
生成對象: 接下來每一個令牌都會被轉換成定義其屬性和規則的對象(這個對象就是節點對象)。
構建完畢: DOM
樹構建完成,整個對象集合就像是一棵樹形結構。可能有人會疑惑爲何DOM
是一個樹形結構,這是由於標籤之間含有複雜的父子關係,樹形結構正好能夠詮釋這個關係(CSSOS
同理,層疊樣式也含有父子關係。例如: div p {font-size: 18px}
,會先尋找全部p
標籤並判斷它的父標籤是否爲div
以後纔會決定要不要採用這個樣式進行渲染)。
整個DOM
樹的構建過程其實就是: 字節 -> 字符 -> 令牌 -> 節點對象 -> 對象模型,下面將經過一個示例HTML
代碼與配圖更形象地解釋這個過程。
<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
中的內容以下:
body { font-size: 16px }
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }複製代碼
瀏覽器得到外部CSS
文件的數據後,就會像構建DOM
樹同樣開始構建CSSOM
樹,這個過程沒有什麼特別的差異。
若是想要更詳細地去體驗一下關鍵渲染路徑的構建,可使用Chrome
開發者工具中的Timeline
功能,它記錄了瀏覽器從請求頁面資源一直到渲染的各類操做過程,甚至還能夠錄製某一時間段的過程(建議不要去看太大的網站,信息會比較雜亂)。
在構建了DOM
樹和CSSOM
樹以後,瀏覽器只是擁有了兩個互相獨立的對象集合,DOM
樹描述了文檔的結構與內容,CSSOM
樹則描述了對文檔應用的樣式規則,想要渲染出頁面,就須要將DOM
樹與CSSOM
樹結合在一塊兒,這就是渲染樹。
瀏覽器會先從DOM
樹的根節點開始遍歷每一個可見節點(不可見的節點天然就不必渲染到頁面了,不可見的節點還包括被CSS
設置了display: none
屬性的節點,值得注意的是visibility: hidden
屬性並不算是不可見屬性,它的語義是隱藏元素,但元素仍然佔據着佈局空間,因此它會被渲染成一個空框)。
對每一個可見節點,找到其適配的CSS
樣式規則並應用。
渲染樹構建完成,每一個節點都是可見節點而且都含有其內容和對應規則的樣式。
渲染樹構建完畢後,瀏覽器獲得了每一個可見節點的內容與其樣式,下一步工做則須要計算每一個節點在窗口內的確切位置與大小,也就是佈局階段。
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秒,但瀏覽器其實已經作了上述所講的很是多的工做,總結一下瀏覽器關鍵渲染路徑的整個過程:
處理HTML
標記數據並生成DOM
樹。
處理CSS
標記數據並生成CSSOM
樹。
將DOM
樹與CSSOM
樹合併在一塊兒生成渲染樹。
遍歷渲染樹開始佈局,計算每一個節點的位置信息。
將每一個節點繪製到屏幕。
瀏覽器想要渲染一個頁面就必須先構建出DOM
樹與CSSOM
樹,若是HTML
與CSS
文件結構很是龐大與複雜,這顯然會給頁面加載速度帶來嚴重影響。
所謂渲染阻塞資源,便是對該資源發送請求後還須要先構建對應的DOM
樹或CSSOM
樹,這種行爲顯然會延遲渲染操做的開始時間。HTML
、CSS
、JavaScript
都是會對渲染產生阻塞的資源,HTML
是必需的(沒有DOM
還談何渲染),但還能夠從CSS
與JavaScript
着手優化,儘量地減小阻塞的產生。
若是可讓CSS
資源只在特定條件下使用,這樣這些資源就能夠在首次加載時先不進行構建CSSOM
樹,只有在符合特定條件時,纔會讓瀏覽器進行阻塞渲染而後構建CSSOM
樹。
CSS
的媒體查詢正是用來實現這個功能的,它由媒體類型以及零個或多個檢查特定媒體特徵情況的表達式組成。
<!-- 沒有使用媒體查詢,這個css資源會阻塞渲染 -->
<link href="style.css" rel="stylesheet">
<!-- all是默認類型,它和不設置媒體查詢的效果是同樣的 -->
<link href="style.css" rel="stylesheet" media="all">
<!-- 動態媒體查詢, 將在網頁加載時計算。 根據網頁加載時設備的方向,portrait.css 可能阻塞渲染,也可能不阻塞渲染。-->
<link href="portrait.css" rel="stylesheet" media="orientation:portrait">
<!-- 只在打印網頁時應用,所以網頁首次在瀏覽器中加載時,它不會阻塞渲染。 -->
<link href="print.css" rel="stylesheet" media="print">複製代碼
使用媒體查詢可讓CSS
資源不在首次加載中阻塞渲染,但無論是哪一種CSS
資源它們的下載請求都不會被忽略,瀏覽器仍然會先下載CSS文件
當瀏覽器的HTML
解析器遇到一個script
標記時會暫停構建DOM
,而後將控制權移交至JavaScript
引擎,這時引擎會開始執行JavaScript
腳本,直到執行結束後,瀏覽器纔會從以前中斷的地方恢復,而後繼續構建DOM
。每次去執行JavaScript
腳本都會嚴重地阻塞DOM
樹的構建,若是JavaScript
腳本還操做了CSSOM
,而正好這個CSSOM
尚未下載和構建,瀏覽器甚至會延遲腳本執行和構建DOM
,直至完成其CSSOM
的下載和構建。顯而易見,若是對JavaScript
的執行位置運用不當,這將會嚴重影響渲染的速度。
下面代碼中的JavaScript
腳本並不會生效,這是由於DOM
樹尚未構建到<p>
標籤時,JavaScript
腳本就已經開始執行了。這也是爲何常常有人在HTML
文件的最下方寫內聯JavaScript
代碼,又或者使用window.onload()
和JQuery
中的$(function(){})
(這兩個函數有一些區別,window.onload()
是等待頁面徹底加載完畢後觸發的事件,而$(function(){})
在DOM
樹構建完畢後就會執行)。
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="style.css" rel="stylesheet">
<title>Hello,World</title>
<script type="text/javascript"> var p = document.getElementsByTagName('p')[0]; p.textContent = 'SylvanasSun'; </script>
</head>
<body>
<p>Hello,World!</p>
</body>
</html>複製代碼
使用async
能夠通知瀏覽器該腳本不須要在引用位置執行,這樣瀏覽器就能夠繼續構建DOM
,JavaScript
腳本會在就緒後開始執行,這樣將顯著提高頁面首次加載的性能(async
只能夠在src
標籤中使用也就是外部引用的JavaScript
文件)。
<!-- 下面2個用法效果是等價的 -->
<script type="text/javascript" src="demo_async.js" async="async"></script>
<script type="text/javascript" src="demo_async.js" async></script>複製代碼
上文已經完整講述了瀏覽器是如何渲染頁面的以及渲染以前的準備工做,接下來咱們如下面的案例來總結一下優化關鍵渲染路徑的方法。
假設有一個HTML
頁面,它只引入了一個CSS
外部文件:
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="style.css" rel="stylesheet">
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg"></div>
</body>
</html>複製代碼
它的關鍵渲染路徑以下:
首先瀏覽器要先對服務器發送請求得到HTML
文件,獲得HTML
文件後開始構建DOM
樹,在碰見<link>
標籤時瀏覽器須要向服務器再次發出請求來得到CSS
文件,而後則是繼續構建DOM
樹和CSSOM
樹,瀏覽器合併出渲染樹,根據渲染樹進行佈局計算,執行繪製操做,頁面渲染完成。
有如下幾個用於描述關鍵渲染路徑性能的詞彙:
關鍵資源:可能阻塞網頁首次渲染的資源(上圖中爲2個,HTML
文件與外部CSS
文件style.css
)。
關鍵路徑長度: 獲取關鍵資源所需的往返次數或總時間(上圖爲2次或以上,一次獲取HTML
文件,一次獲取CSS
文件,這個次數基於TCP
協議的最大擁塞窗口,一個文件不必定能在一次鏈接內傳輸完畢)。
關鍵字節:全部關鍵資源文件大小的總和(上圖爲9KB
)。
接下來,案例代碼的需求發生了變化,它新增了一個JavaScript
文件。
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="style.css" rel="stylesheet">
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg"></div>
<script src="app.js"></script>
</body>
</html>複製代碼
JavaScript
文件阻塞了DOM
樹的構建,而且在執行JavaScript
腳本時還須要先等待構建CSSOM
樹,上圖的關鍵渲染路徑特性以下:
關鍵資源: 3(HTML
、style.css
、app.js
)
關鍵路徑長度: 2或以上(瀏覽器會在一次鏈接中一塊兒下載style.css
和app.js
)
關鍵字節:11KB
如今,咱們要優化關鍵渲染路徑,首先將<script>
標籤添加異步屬性async
,這樣瀏覽器的HTML
解析器就不會阻塞這個JavaScript
文件了。
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="style.css" rel="stylesheet">
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg"></div>
<script src="app.js" async></script>
</body>
</html>複製代碼
關鍵資源:2(app.js
爲異步加載,不會成爲阻塞渲染的資源)
關鍵路徑長度: 2或以上
關鍵字節: 9KB(app.js
再也不是關鍵資源,因此沒有算上它的大小)
接下來對CSS
進行優化,好比添加上媒體查詢。
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="style.css" rel="stylesheet" media="print">
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg"></div>
<script src="app.js" async></script>
</body>
</html>複製代碼
關鍵資源:1(app.js
爲異步加載,style.css
只有在打印時纔會使用,因此只剩下HTML
一個關鍵資源,也就是說當DOM
樹構建完畢,瀏覽器就會開始進行渲染)
關鍵路徑長度:1或以上
關鍵字節:5KB
優化關鍵渲染路徑就是在對關鍵資源、關鍵路徑長度和關鍵字節進行優化。關鍵資源越少,瀏覽器在渲染前的準備工做就越少;一樣,關鍵路徑長度和關鍵字節關係到瀏覽器下載資源的效率,它們越少,瀏覽器下載資源的速度就越快。
除了異步加載JavaScript
和使用媒體查詢外還有不少其餘的優化方案可使頁面的首次加載變得更快,這些方案能夠綜合起來使用,但核心的思想仍是針對關鍵渲染路徑進行了優化。
服務端在接收到請求時先只響應回HTML
的初始部分,後續的HTML
內容在須要時再經過AJAX
得到。因爲服務端只發送了部分HTML
文件,這讓構建DOM
樹的工做量減小不少,從而讓用戶感受頁面的加載速度很快。
注意,這個方法不能用在CSS
上,瀏覽器不容許CSSOM
只構建初始部分,不然會沒法肯定具體的樣式。
經過對外部資源進行壓縮能夠大幅度地減小瀏覽器須要下載的資源量,它會減小關鍵路徑長度與關鍵字節,使頁面的加載速度變得更快。
對數據進行壓縮其實就是使用更少的位數來對數據進行重編碼。現在有很是多的壓縮算法,且每個的做用領域也各不相同,它們的複雜度也不相同,不過在這裏我不會講壓縮算法的細節,感興趣的朋友能夠本身Google。
在對HTML
、CSS
和JavaScript
這些文件進行壓縮以前,還須要先進行一次冗餘壓縮。所謂冗餘壓縮,就是去除多餘的字符,例如註釋、空格符和換行符。這些字符對於程序員是有用的,畢竟沒有格式化的代碼可讀性是很是恐怖的,但它們對於瀏覽器是沒有任何意義的,去除這些冗餘能夠減小文件的數據量。在進行完冗餘壓縮以後,再使用壓縮算法進一步對數據自己進行壓縮,例如GZIP
(GZIP
是一個能夠做用於任何字節流的通用壓縮算法,它會記憶以前已經看到的內容,而後再嘗試查找並替換重複的內容。)。
經過網絡來獲取資源一般是緩慢的,若是資源文件過於膨大,瀏覽器還須要與服務器之間進行屢次往返通訊才能得到完整的資源文件。緩存能夠複用以前獲取的資源,既而後端可使用緩存來減小訪問數據庫的開銷,那前端天然也可使用緩存來複用資源文件。
瀏覽器自帶了HTTP
緩存的功能,只須要確保每一個服務器響應的頭部都包含了如下的屬性:
ETag: ETag是一個傳遞驗證令牌,它對資源的更新進行檢查,若是資源未發生變化時不會傳送任何數據。當瀏覽器發送一個請求時,會把ETag一塊兒發送到服務器,服務器會根據當前資源覈對令牌(ETag一般是對內容進行Hash
後得出的一個指紋),若是資源未發生變化,服務器將返回304 Not Modified
響應,這時瀏覽器沒必要再次下載資源,而是繼續複用緩存。
Cache-Control: Cache-Control定義了緩存的策略,它規定在什麼條件下能夠緩存響應以及能夠緩存多久。
no-cache: no-cache表示必須先與服務器確認返回的響應是否發生了變化,而後才能使用該響應來知足後續對同一網址的請求(每次都會根據ETag對服務器發送請求來確認變化,若是未發生變化,瀏覽器不會下載資源)。
no-store: no-store直接禁止瀏覽器以及全部中間緩存存儲任何版本的返回響應。簡單的說,該策略會禁止任何緩存,每次發送請求時,都會完整地下載服務器的響應。
public&private: 若是響應被標記爲public,則即便它有關聯的HTTP
身份驗證,甚至響應狀態代碼一般沒法緩存,瀏覽器也能夠緩存響應。若是響應被標記爲private,那麼這個響應一般只爲單個用戶緩存,所以不容許任何中間緩存(CDN)對其進行緩存,private通常用在緩存用戶私人信息頁面。
max-age: max-age定義了從請求時間開始,緩存的最長時間,單位爲秒。
Pre-fetching
是一種提示瀏覽器預先加載用戶以後可能會使用到的資源的方法。
使用dns-prefetch
來提早進行DNS
解析,以便以後能夠快速地訪問另外一個主機名(瀏覽器會在加載網頁時對網頁中的域名進行解析緩存,這樣你在以後的訪問時無需進行額外的DNS解析,減小了用戶等待時間,提升了頁面加載速度)。
<link rel="dns-prefetch" href="other.hostname.com">複製代碼
使用prefetch
屬性能夠預先下載資源,不過它的優先級是最低的。
<link rel="prefetch" href="/some_other_resource.jpeg">複製代碼
Chrome
容許使用subresource
屬性指定優先級最高的下載資源(當全部屬性爲subresource
的資源下載完完畢後,纔會開始下載屬性爲prefetch
的資源)。
<link rel="subresource" href="/some_other_resource.js">複製代碼
prerender
能夠預先渲染好頁面並隱藏起來,以後打開這個頁面會跳過渲染階段直接呈如今用戶面前(推薦對用戶接下來必須訪問的頁面進行預渲染,不然得不償失)。
<link rel="prerender" href="//domain.com/next_page.html">複製代碼