本文主要介紹「關鍵渲染路徑」與「網絡」兩個方面的性能優化並提供demo,篇幅較長建議電腦觀看。php
前端優化的方面太多,本文介紹的僅僅是其中的一部分,力求涵蓋「關鍵渲染路徑」的方方面面,及一些不常被提到的「網絡優化」部分。css
測試環境如無特殊說明均爲Chrome 57html
瀏覽器從打開一個URL到渲染完頁面共有:前端
下載HTML文檔html5
下載HTML文檔中的csswebpack
下載Js文件git
執行js腳本github
下載其餘資源web
經過HTML文檔構建DOM(Parse HTML)瀏覽器
經過CSS文件構建CSSOM(Parse CSS)
經過DOM與CSSOM計算render tree
根據render tree進行繪製,計算各個元素位置與大小(Layout)
對頁面進行上色,渲染爲最終顯示的像素(Paint)
第一次完成Paint稱爲「初次渲染」,這時候用戶就能看到render tree裏面的東西了。而完成初次渲染的過程稱爲「關鍵渲染路徑」,關鍵渲染路徑上須要加載的資源叫作「關鍵資源」
這個過程不少很複雜,其中的依賴關係也很複雜,筆者嘗試畫圖來表示,可是實在是沒畫出來,因此仍是用文字來表述吧:
引入的資源,哪怕被阻塞(好比被js腳本阻塞後續link標籤),瀏覽器依舊會智能的預先加載它們(可是不執行)
「CSS文件的加載」會阻塞「Js文件執行」。若CSS引用在Js文件以前,「加載CSS文件」會阻塞「Js文件執行」。即CSS文件未加載解析完成前,js文件不會獲得執行。由於js有可能會修改CSSOM。帶有async和defer屬性的script不受限制。
Parse HTML的解析是增量的,所以瀏覽器能夠邊下載HTML邊構建DOM樹
「CSS文件的加載」會阻塞「Layout」。若頁面有正在加載的CSS文件,在CSS文件加載完以前,瀏覽器不會對頁面進行Layout,這是爲了防止樣式突變帶來的抖動
「加載Js文件」會阻塞「Parse HTML」,這個估計你們都知道了,由於js能夠經過document.write修改HTML文檔流
「Js文件執行」會幾乎會阻塞全部東西,包括Layout
比較有意思的是,字體的加載會阻塞局部的渲染。若某一段文本的字體使用了一個還沒有加載完的字體,這段文本則先不會被Paint,直到字體加載完或者超過某個時間(一般是3秒)文本纔會忽然顯示。
瀏覽器爲了不FOUT(Flash Of Unstyled Text),會儘可能等待字體加載完成後,再顯示應用了該字體的內容。只有當字體超過一段時間仍未加載成功時,瀏覽器纔會降級使用系統字體。每一個瀏覽器都規定了本身的超時時間(Chrome是3秒)。但這也帶來了FOIT(Flash Of Invisible Text)問題。內容沒法儘快地被展現,致使空白
CSS會阻塞Layout:Demo
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <link rel="stylesheet" href="style.css" /> <!-- 這個css文件會加載3秒鐘,在這個css加載完成前瀏覽器不會layout --> <link rel="stylesheet" href="../conn/sleep.php?sleep=3&content=h2{color:red;}" /> <title>Title</title> </head> <body> <h1>Hello</h1> <h2>World</h2> </body> </html>
CSS會阻塞Js執行:Demo
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <link rel="stylesheet" href="style.css" /> <!-- 這個css文件會加載3秒鐘 --> <link rel="stylesheet" href="../conn/sleep.php?sleep=3&content=h2{color:red;}" /> <script> // 這段js會等待css加載完纔會運行 alert('js is run!'); </script> <title>Title</title> </head> <body> <h1>Hello</h1> <h2>World</h2> </body> </html>
Js執行會阻塞關鍵渲染路徑,哪怕是defer仍是async:Demo
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <script> function sleep(ms){ var ts =+new Date; while(true){ if(+new Date -ts >=ms) break; } return +new Date -ts; } </script> <!-- 這個css文件會加載2秒鐘,因此會在js文件以後加載完 --> <link rel="stylesheet" href="../conn/sleep.php?sleep=2&content=h2{color:red;}" /> <!-- 這個js文件會瞬間加載完,可是會運行3秒鐘 --> <script defer src="run3s.js"></script> <!-- 這個js文件會瞬間加載完,可是會運行2秒鐘 --> <script async src="run2s.js"></script> <title>Title</title> </head> <body> <!-- 打開頁面後5秒鐘纔會顯示,由於js執行會阻塞關鍵渲染路徑 --> <h1>Hello</h1> <h2>World</h2> </body> </html>
Foot會阻塞局部渲染,可是智能的瀏覽器會給他設定一個上限,通常是3秒鐘:Demo
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <style> @font-face { font-family: "test-font"; src: url("../conn/sleep.php?sleep=5&file=scripts_.ttf"); } h1{ font-family: "test-font"; } </style> <title>Title</title> </head> <body> <h1>Hello</h1> <h2>World</h2> </body> </html>
優化核心概念是:將初次渲染不須要的CSS想辦法剝離出關鍵渲染路徑
若是僅僅是爲了提早初次渲染時間而進行優化,將頁面必備的CSS剝離關鍵渲染路徑而形成樣式突變致使頁面抖動,則得不償失了
對某些媒體查詢條件觸發後才使用的css,能夠在link標籤中加入media
屬性,以下:
<link rel="stylesheet" href="index_print.css" media="print">
此樣式表仍會加載。當瀏覽器環境不匹配媒體查詢條件時,該樣式表不會阻塞渲染。咱們可針對不一樣媒體環境拆分CSS文件,併爲link標籤添加媒體查詢,避免爲了加載非關鍵CSS資源,而阻塞初次渲染
可使用js代碼來添加css
var style = document.createElement('link'); style.rel = 'stylesheet'; style.href = 'index.css'; document.head.appendChild(style);
將link標籤的rel屬性設置爲preload
,瀏覽器遇到遇到標記爲preload的link時,會開始加載它,可是因爲rel不是stylesheet
,所以不會阻塞渲染。
<link rel="preload" href="index_print.css" as="style" onload="this.rel='stylesheet'">
而後在適當的時候,在rel改成stylesheet,便可應用此樣式。
可是這個屬性兼容性比較差,詳細能夠參考這裏。不過有一個polyfill能夠用loadCSS,原理是經過DOM API插入樣式資源。
這個屬性的使用情景有些偏,也多是我理解問題:
當使用preload引入css文件時,實際上證實這個頁面根本不須要這個css,它有多是打印樣式,或者是響應式網站的另外一套css代碼。可是,使用preload屬性,瀏覽器反而會預先加載它,也就是說,在window.onload以前,用戶將耗費了網絡資源在加載一個暫時不須要的樣式。網絡資源不多是無限的,也就是說這個css會佔用頁面其餘資源好比圖片的網絡資源。
詢問瓜瓜老師本人後,瓜瓜老師說:
舉個例子。第三屏有個廣告版,它的樣式
這樣確實這個css的緊急程度就介於關鍵渲染路徑的css與頁面圖片之間了,不過貌似這個情景很受限。
當script標籤擁有defer屬性時,該腳本會被推遲到整個HTML文檔解析完後,再開始執行。所以將腳本放在head中,能夠提前瀏覽器對腳本文件的加載,可是卻不會阻塞parse HTML。
<script src="index.js" defer></script> <!-- 百度統計代碼 --> <script src="tongji.js" defer></script>
注意,defer的腳本不會被css阻塞,parse HTML完成後當即執行,可是有可能會阻塞關鍵渲染路徑。爲何說有可能呢,假如腳本文件在render tree生成前加載完畢,則會開始執行,執行過程當中會阻塞關鍵渲染路徑。請參考這個Demo
被defer的腳本,在執行時會嚴格按照在HTML文檔中出現的順序執行,可是實際上貌似不是這樣,js文件先後文件如有依賴需慎重使用。
和defer相似,只是當js加載完後立刻執行,而不在意parse HTML是否完成,所以假如腳本比css先加載完,也會阻塞關鍵渲染路徑。
<script src="index.js" defer></script> <!-- 百度統計代碼 --> <script src="tongji.js" defer></script>
據筆者所知,這是惟一一種100%不會阻塞關鍵渲染路徑的js腳本加載方式。經過DOM API引入的js腳本會等到頁面Layout和Paint後再開始執行,不論你將載入js文件的代碼放在head中仍是body後面亦是如此。
若不想讓字體阻塞局部渲染,可以使用Web Font Loader
網絡優化和CSS優化策略相同,儘量讓關鍵資源提早加載完,因此優化時儘可能將如下指標壓縮到最低:
關鍵資源數
關鍵資源體積
關鍵資源網絡來回數
固然,若是你的項目使用了先進的SPDY或HTTP/2,下面的方法可能並不適用。
RFC2616規定同域名同時只能有 2 個鏈接(RFC7230 中無限制),而現代瀏覽器通常容許同域6個併發鏈接。所以,當頁面中有許多須要外鏈的資源(script、link等),瀏覽器最多在每一個域同時併發下載6個。
每個請求,若使用域名,則須要額外增長一次DNS查詢時間(若緩存未過時會命中緩存),所以一個網站過多的使用不一樣域名的資源會額外增長DNS查詢開銷,這點在移動端很是明顯。
固然,每一個請求創建根據TCP協議規定,還須要先進行3次捂手才能夠創建連接。
儘量的合併請求,減小網絡請求數。這一點可能在其餘性能優化文章都說爛了:
小圖片轉base64
合併打包CSS、JS文件
如今的比較流行的webpack就很是擅長作這種事情
使用內聯的CSS和JS當然能夠減小請求,可是使用內聯也意味着你的CSS和JS將不會再被瀏覽器緩存,所以要適度的使用內聯,內聯不是萬能的。
最佳方案確定是過渡到HTTP/2無疑,可是如今HTTP/2的支持並不算太好,並且各大瀏覽器僅支持TLS下實現的HTTP/2(說白了就是HTTPS),使得HTTP/2的使用存在許些限制。
若是沒有HTTP/2,或許能夠:
使用Keep-Alive
能夠規避TCP三次握手的時間
使用Transfer-Encoding:chunked
分塊輸出文件,還記得parse HTML的過程是增量的嗎?若瀏覽器能夠邊下載HTML文件邊解析,豈不美哉?
減小重定向,這個看上去理所固然可是實際上卻很容易被忽略
瀏覽器同域並行下載數量有限,因此只要多創建幾個二級域名就行了,而後合理的分配各個資源就行了。
假如因爲某些不可抗拒緣由,關鍵資源數是12個,那麼只要創建2個二級域名分別分配給其中的12個資源,瀏覽器會同時並行下載它們了。
不過,使用域名散列要適度,每個域名都須要額外的增長一次DNS查詢時間。固然,DNS自己也有緩存,或許適當的增長DNS TTL時間也是個不錯的主意。
對於js、css文件,如今網上現成的壓縮工具一堆,並且應用十分普遍,相信你們都知道了,這裏就很少說了。
說到壓縮,服務器開啓必定的壓縮策略(如gzip)是個不錯的主意,效果拔羣,資源大概會壓縮到原有的1/3左右。
圖片壓縮,這個須要知道什麼情境下適合什麼類型的圖片,GIF、JPG、PNG使用情景各不相同,具體能夠參考這篇文章:圖片格式那麼多,哪一種更適合你?
假如一個頁面須要引入2個CSS才能工做,下面有2種方式
2個均用link引入
1個用link引入,在css中import另外一個css
毫無疑問確定是前者快,由於前者的網絡來回數是1,然後者是2。
所以,儘量將資源加載扁平化,減小關鍵資源網絡來回數是個不錯的主意。
固然,優化時要注意的點也有很多,好比前面提到的瀏覽器同域併發限制等,須要權衡使其不要影響到其餘的致使初次渲染時間延後。
使用document.write
打印link標籤引入css仍會阻塞初次渲染。
奇舞團@瓜瓜老師:
奇舞團@屈屈老師:
W3C規範: