性能優化涵蓋的範圍很是之廣,其中包含的知識也很是繁雜。從加載性能到渲染性能、運行時性能,每一個點都有很是多能夠學習與實踐的知識。javascript
優化問題包含方方面面,優化手段也依場景和具體問題而定。所以,本文並非一個泛而全的概覽文章,而是以以前的一次對於業務產品的簡單優化(主要是DOMContentLoaded時間)爲例,介紹瞭如何使用Chrome Dev Tools來分析問題,使用一些策略來縮短DOMContentLoaded的時間,提升加載速度。html
W3C將頁面加載分爲了許多階段, DOMContentLoaded(如下簡稱DCL)相似的有一些 DOM readState ,它們都會標識頁面的加載狀態與所處的階段。咱們接觸最多的也就是 readState 中的 interactive、complete(或load事件)以及DCL事件html5
簡單瞭解一下它們。瀏覽器會基於HTML內容來構建DOM,並基於CSS構建CSSOM。二者構建完成後,會合併爲Render Tree。當DOM構建完畢後, document.readyState
狀態會變爲 interactive
。java
Render Tree構建完成就會進入到咱們很是熟悉的 Layout –>> Paint –>> Composite 管道。web
可是當頁面包含Javascript時,這個過程會有些區別。chrome
根據HTML5 spec,因爲在Javascript中能夠訪問DOM,所以當瀏覽器解析頁面遇到Javascript後會阻塞 DOM 的解析;於此同時,爲避免CSS與Javascript之間的競態,CSSOM的構建會阻塞 Javascript 腳本的執行。不過有一個例外,若是將腳本設置爲async,會有一個區別,DCL的觸發不須要等待async的腳本被執行。瀏覽器
也就是:緩存
interactive
。即 "DOM tree is ready"。DOMContentLoaded
事件。即 "CSSOM is ready"。或者將上面的部分精簡一下:性能優化
DOM construction can’t proceed until JavaScript is executed, and JavaScript can’t proceed until CSSOM is available. [1]bash
下面就能夠經過Chrome Dev Tools來分析問題。爲了內容精簡,如下截圖取了在slow 3G 無緩存模式下的訪問狀況,爲了保持和線上環境相似(還原瀏覽器的同源最大請求併發),在本地搭建對應的服務器放置靜態資源。wifi狀況下,各個時間點大體等比縮短8~9倍。
首先看一個總體的waterfall
在最下面能夠看到 DCL 爲 17.00s(slow 3G)。
p.s. 頁面load時間也很長。主要由於業務膨脹後,頁面包含過多資源,沒有使用一些懶加載與異步渲染技術,這部分也存在不少優化空間,但因爲篇幅不在本文中討論內。
頁面裏有一個很明顯的請求block了DCL —— common.js。那麼common.js是什麼呢?它其實就是項目中一些通用腳本文件的打包合併。
因爲common.js爲同步腳本,所以等到它其下載並執行完畢後,纔會觸發DCL。而與此對應的,其餘各個腳本的時間線與其有很大差距。具體來看common.js的Timing pharse,耗時11.44s,其中download花費 7.12s。
download過長最直接的緣由就是文件太大。common.js的打包合併包含了下面的內容
'pkg/common.js': [
'static/js/bridge.js', // 業務基礎庫
'static/js/zepto.min.js', // 第三方庫
'static/js/zepto.touch.min.js', // 第三方庫
'static/js/bluebird.core.min.js', // 第三方庫
'static/js/link.interceptor.js', // 業務基礎庫
'static/js/global.js', // 業務基礎庫
'static/js/felog.js', // 業務基礎庫
'widget/utils/*.js' // 業務工具組件
]
複製代碼
這裏,咱們發現這麼打包會存在下面幾個問題:
download過長最直接的緣由:文件過大。
將這些資源所有打包在一塊兒致使common.js較大,原文件161KB,gzip 以後爲52.5KB,單點阻塞了關鍵渲染路徑。你也能夠在 audits 中的Critical Request Chains部分發現common.js是瓶頸。
zepto/bluebird這種第三方庫屬於很是穩定的資源,幾乎不會改動。雖然代碼量較多,可是經過HTTP Cache能夠有效避免重複下載。同時,上線新版後,爲了不一些文件走 HTTP Cache,咱們會給靜態資源加上 md5。
然而,當這些穩定的第三方庫與一些其餘文件打包後,會由於該打包中某些文件的局部變更致使合併打包後的hash變化而緩存失效。
例如,其中bridge.js與/utils/*.js容易隨着版本上線迭代,迭代後打包致使common的hash變化,HTTP Cache失效,zepto/bluebird等較大的資源雖然未更改,但因爲打包在了一塊兒,仍須要從新下載。每次上線新版本後,一些加載的性能數據表現都會顯著降低,其中一部分緣由在於此。
結合上面分析的問題,能夠進行一些簡單而有效的優化。
考慮將文件的打包合併按照文件的更新頻率進行劃分。這樣既能夠有效縮減common.js的大小,也能夠基於不一樣類型的資源,更好利用HTTP Cache。
例如:
將基本不會變更的文件打包爲 lib.js,主要爲一些第三方庫,這類文件幾乎不會改動,很是穩定。
將項目依賴的最基礎js打包爲common.js,例如本文中的global.js、link.interceptor.js,項目中的全部部分都須要它們,同時也是項目特有的,相較上一部分的lib會有必定量的開發與改動,可是更新間隔可能會有幾個版本。
將項目中變更較爲頻繁的工具庫打包爲util.js,理論上其中工具因爲不做爲基礎運行的依賴,是能夠異步加載的。這部分代碼是三者之中變更最爲頻繁的。
'pkg/util.js': [
'widget/utils/*.js'
],
'pkg/common.js': [
'static/js/link.interceptor.js',
'static/js/global.js',
'static/js/felog.js'
],
'pkg/lib.js': [
'static/js/zepto.min.js',
'static/js/zepto.touch.min.js',
'static/js/bluebird.core.min.js'
]
複製代碼
可是在拆分後DCL時間幾乎沒有減小。
這裏就不得不提到打包的初衷之一:減小併發。咱們將common.js拆分爲三個部分後,觸碰到了同域TCP鏈接數限制,圖中的這四個資源被chrome放入了隊列(圖中白色長條)。
Queueing. The browser queues requests when:
- There are higher priority requests.
- There are already six TCP (Chrome) connections open for this origin, which is the limit. Applies to HTTP/1.0 and HTTP/1.1 only.
- The browser is briefly allocating space in the disk cache
咱們打包合併資源必定程度上也是爲了減小TCP round trip,同時儘可能規避同域下的請求併發數量限制。所以在common.js拆分時,也要注意不宜分得過細,不然過猶不及,忘了初衷。
從network waterfall中也很容易發現,大部分資源因爲size較小,其下載時間其實很是短,耗時主要是在TTFB(Time To First Byte),能夠粗略理解爲在等待服務器返回數據(圖中表現出來就是綠色較多)。因此除了打包項目依賴的lib.js/common.js/util.js外,還能夠考慮將部分依賴的組件腳本進行打包合併,
像上圖中這四個腳本的耗時都在在TTFB上,並且在同一個CDN上,能夠經過打包減少沒必要要的併發。將首屏依賴的關鍵組件進行打包:
'pkg/util.js': [
'widget/utils/*.js'
],
'pkg/common.js': [
'static/js/bridge.js',
'static/js/link.interceptor.js',
'static/js/global.js',
'static/js/felog.js'
],
'pkg/lib.js': [
'static/js/zepto.min.js',
'static/js/zepto.touch.min.js',
'static/js/bluebird.core.min.js'
],
'pkg/homewgt.js': [
'widget/home/**.js',
'widget/player/*.js',
]
複製代碼
優化後的DCL變爲了11.20s。
注意,一些打包工具會自動分析文件依賴關係,文件打包後會同時替換資源路徑。例如:在HTML中,引用了 static/js/zepto.min.js
和 static/js/bluebird.core.min.js
兩個資源,在打包後構建工具會將HTML中的引用自動替換爲 lib.js
。所以須要注意打包後的資源加載順序。
例如,原HTML中的資源順序
<script type="text/javascript" src="//your.cdn.com/static/js/bridge.js"></script>
<script type="text/javascript" src="//your.cdn.com/static/js/zepto.min.js"></script>
<script type="text/javascript" src="//your.cdn.com/static/js/bluebird.core.min.js"></script>
<script type="text/javascript" src="//your.cdn.com/static/js/global.js"></script>
複製代碼
其中 global.js
依賴於 zepto.min.js
,這個在目前看來沒有問題。可是因爲打包合併,構建工具會自動替換腳本文件名。因爲 bridge.js
的位置,在打包後common.js
的引入順序先於lib.js
。這就致使 global.js
先於 zepto.min.js
引入與執行,出現錯誤。
對此,在不影響原有依賴的狀況下,能夠調整腳本順序
<script type="text/javascript" src="//your.cdn.com/static/js/zepto.min.js"></script>
<script type="text/javascript" src="//your.cdn.com/static/js/bluebird.core.min.js"></script>
<script type="text/javascript" src="//your.cdn.com/static/js/bridge.js"></script>
<script type="text/javascript" src="//your.cdn.com/static/js/global.js"></script>
複製代碼
輸出的結果以下:
最終在無緩存的slow 3G下DCL時間11.19s,相比最初的17.00s,下降34%。(wifi狀況降低比例相同,時間大體同比爲1/8~1/9,接近1s)。同時,相較於以前,一些靜態資源可以更好地去利用HTTP Cache,節省帶寬,下降每次新版上線後用戶訪問站點的靜態資源下載量。
須要指出,性能優化也許有一些「基本準則」,但絕對沒有銀彈。不管是多麼「基礎與通用」的優化手段,亦或是多麼「複雜而有針對性」的優化手段,都是在解決特定的具體問題。所以,解決性能問題每每都是從實際出發,經過「排查問題 --> 分析診斷 --> 實施優化 --> 驗證效果」這樣一條不斷循環的路徑來開展的。
同時,提高性能的其中一個目的就是更好的用戶體驗。用戶體驗每每是一個寬泛的概念,涉及方方面面。相對應的,性能優化也不能只死盯着某個「指標」,更應該理解其背後對產品與用戶的意義。從問題出發,拿數據量化,找解決方案。
在實際環境下,面對有限的資源和各類限制,創造最大的價值。性能優化更是如此。