【性能優化實踐】優化打包策略提高頁面加載速度

TL;DR

  • 能夠考慮基於HTTP Cache來定義打包維度,將Cache週期相同的script儘可能打包在一塊兒,最大限度利用Cache;
  • 合併零散的小腳本,避免觸發瀏覽器併發請求限制後,資源請求串行,TTFB疊加等待時間;
  • 注意打包後的資源依賴與資源引入順序。

1. 引言

性能優化涵蓋的範圍很是之廣,其中包含的知識也很是繁雜。從加載性能到渲染性能、運行時性能,每一個點都有很是多能夠學習與實踐的知識。javascript

優化問題包含方方面面,優化手段也依場景和具體問題而定。所以,本文並非一個泛而全的概覽文章,而是以以前的一次對於業務產品的簡單優化(主要是DOMContentLoaded時間)爲例,介紹瞭如何使用Chrome Dev Tools來分析問題,使用一些策略來縮短DOMContentLoaded的時間,提升加載速度。html

2. DOMContentLoaded事件

W3C將頁面加載分爲了許多階段, DOMContentLoaded(如下簡稱DCL)相似的有一些 DOM readState ,它們都會標識頁面的加載狀態與所處的階段。咱們接觸最多的也就是 readState 中的 interactive、complete(或load事件)以及DCL事件html5

簡單瞭解一下它們。瀏覽器會基於HTML內容來構建DOM,並基於CSS構建CSSOM。二者構建完成後,會合併爲Render Tree。當DOM構建完畢後, document.readyState 狀態會變爲 interactivejava

Render Tree構建完成就會進入到咱們很是熟悉的 Layout –>> Paint –>> Composite 管道。web

可是當頁面包含Javascript時,這個過程會有些區別。chrome

根據HTML5 spec,因爲在Javascript中能夠訪問DOM,所以當瀏覽器解析頁面遇到Javascript後會阻塞 DOM 的解析;於此同時,爲避免CSS與Javascript之間的競態,CSSOM的構建會阻塞 Javascript 腳本的執行。不過有一個例外,若是將腳本設置爲async,會有一個區別,DCL的觸發不須要等待async的腳本被執行。瀏覽器

也就是:緩存

  • 當瀏覽器完成對於document的解析(parse)時,文檔狀態就會被標記爲 interactive 。即 "DOM tree is ready"。
  • 當全部普通(既不是defer也不是async)與defer的腳本被執行,而且已經沒有任何阻塞腳本的樣式時,瀏覽器就會觸發 DOMContentLoaded 事件。即 "CSSOM is ready"。

或者將上面的部分精簡一下:性能優化

DOM construction can’t proceed until JavaScript is executed, and JavaScript can’t proceed until CSSOM is available. [1]bash

3. 排查問題

下面就能夠經過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。

4. 分析診斷

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' // 業務工具組件
]
複製代碼

這裏,咱們發現這麼打包會存在下面幾個問題:

4.1. 文件大小

download過長最直接的緣由:文件過大。

將這些資源所有打包在一塊兒致使common.js較大,原文件161KB,gzip 以後爲52.5KB,單點阻塞了關鍵渲染路徑。你也能夠在 audits 中的Critical Request Chains部分發現common.js是瓶頸。

4.2. HTTP Cache

zepto/bluebird這種第三方庫屬於很是穩定的資源,幾乎不會改動。雖然代碼量較多,可是經過HTTP Cache能夠有效避免重複下載。同時,上線新版後,爲了不一些文件走 HTTP Cache,咱們會給靜態資源加上 md5。

然而,當這些穩定的第三方庫與一些其餘文件打包後,會由於該打包中某些文件的局部變更致使合併打包後的hash變化而緩存失效。

例如,其中bridge.js與/utils/*.js容易隨着版本上線迭代,迭代後打包致使common的hash變化,HTTP Cache失效,zepto/bluebird等較大的資源雖然未更改,但因爲打包在了一塊兒,仍須要從新下載。每次上線新版本後,一些加載的性能數據表現都會顯著降低,其中一部分緣由在於此。

5. 實施優化手段

結合上面分析的問題,能夠進行一些簡單而有效的優化。

5.1. 拆包

考慮將文件的打包合併按照文件的更新頻率進行劃分。這樣既能夠有效縮減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'
]
複製代碼

5.2 Quene Delay

可是在拆分後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。

5.3 資源引入順序

注意,一些打包工具會自動分析文件依賴關係,文件打包後會同時替換資源路徑。例如:在HTML中,引用了 static/js/zepto.min.jsstatic/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>
複製代碼

輸出的結果以下:

6. 驗證效果

最終在無緩存的slow 3G下DCL時間11.19s,相比最初的17.00s,下降34%。(wifi狀況降低比例相同,時間大體同比爲1/8~1/9,接近1s)。同時,相較於以前,一些靜態資源可以更好地去利用HTTP Cache,節省帶寬,下降每次新版上線後用戶訪問站點的靜態資源下載量。

7. 寫在最後

須要指出,性能優化也許有一些「基本準則」,但絕對沒有銀彈。不管是多麼「基礎與通用」的優化手段,亦或是多麼「複雜而有針對性」的優化手段,都是在解決特定的具體問題。所以,解決性能問題每每都是從實際出發,經過「排查問題 --> 分析診斷 --> 實施優化 --> 驗證效果」這樣一條不斷循環的路徑來開展的。

同時,提高性能的其中一個目的就是更好的用戶體驗。用戶體驗每每是一個寬泛的概念,涉及方方面面。相對應的,性能優化也不能只死盯着某個「指標」,更應該理解其背後對產品與用戶的意義。從問題出發,拿數據量化,找解決方案。

在實際環境下,面對有限的資源和各類限制,創造最大的價值。性能優化更是如此。

參考資料

相關文章
相關標籤/搜索