Vue專案優化實踐 —— CDN + Gzip + Prerender
和很多小夥伴一樣,我在開發 Vue
專案時也是基於官方 vue-cli@2
的 webpack
模版,但隨著專案越做越大,依賴的第三方 npm
包越來越多,構建之後的檔案也會越來越大,尤其是 vendor.js
,甚至會達到 2M
左右。再加上又是單頁應用,這就會導致在網速較慢或者伺服器頻寬有限的情況出現長時間的白屏。為了解決這個問題,我做了一些探索,在幾乎不需要改動業務程式碼的情況下,找到了三種有明顯效果的優化方案 —— CDN
+ Gzip
+ Prerender
。我把這些方法整理了一下,放在了 ofollow,noindex">Github倉庫 上,意圖通過不同的分支來展示不同的優化方式,對 Vue
專案效能的影響。你可以直接克隆下來試一試,也得益於有 git
歷史,你也可以很方便的檢視具體的改動細節。下面我將通過一個簡單的專案來展示這三種優化方案的效果。
一、首先準備一個 簡單的專案
通過 vue-cli@2
的 webpack
模版生成,只包含最基礎的 Vue
三件套 ———— vue
、 vue-router
、 vuex
以及常用的 element-ui
和 axios
。拆分兩個路由——“首頁”和“通訊錄”,通過 axios
非同步獲取一個通訊錄名單,並利用 element-ui
的表格展示。直接 build
,不做任何優化處理,以作參照。
1.1 構建後文件說明:
-
app.css
: 壓縮合並後的樣式檔案。 -
app.js
:主要包含專案中的App.vue
、main.js
、router
、store
等業務程式碼。 -
vendor.js
:主要包含專案依賴的諸如vuex
,axios
等第三方庫的原始碼,這也是為什麼這個檔案如此之大的原因,下一步將探索如何優化這一塊,畢竟隨著專案的開發,依賴的庫也能會越來越多。 -
數字.js
:以0、1、2、3等數字開頭的js
檔案,這些檔案是各個路由切分出的程式碼塊,因為我拆分了兩個路由,並做了路由懶載入,所以出現了0和1兩個js
檔案。 -
mainfest.js
:mainfest
的英文有 清單、名單的意思 ,該檔案包含了載入和處理路由模組的邏輯

1.2 禁用瀏覽器快取,網速限定為 Fast 3G
下的 Network
圖(執行在本地的 nginx
伺服器上
可以看到未經優化的 base
版本在 Fast 3G
的網路下大概需要7秒多的時間才載入完畢

二、 CDN 優化
- 將依賴的
vue
、vue-router
、vuex
、element-ui
和axios
這五個庫,全部改為通過CDN
連結獲取。藉助HtmlWebpackPlugin
,可以方便的使用迴圈語法在index.html
裡插入js
和css
的CDN
連結。這裡的CDN
大部分使用的jsDelivr 提供的。
<!-- CDN檔案,配置在config/index.js下 --> <% for (var i in htmlWebpackPlugin.options.css) { %> <link href="<%= htmlWebpackPlugin.options.css[i] %>" rel="stylesheet"> <% } %> <% for (var i in htmlWebpackPlugin.options.js) { %> <script src="<%= htmlWebpackPlugin.options.js[i] %>"></script> <% } %> 複製程式碼
- 在
build/webpack.base.conf.js
中新增如下程式碼,這使得在使用CDN
引入外部檔案的情況下,依然可以在專案中使用import
的語法來引入這些第三方庫,也就意味著你不需要改動專案的程式碼,這裡的鍵名是import
的npm
包名,鍵值是該庫暴露的全域性變數。webpack文件參考連結。
externals: { 'vue': 'Vue', 'vue-router': 'VueRouter', 'vuex': 'Vuex', 'element-ui':'ELEMENT', 'axios':'axios' } 複製程式碼
- 解除安裝依賴的
npm
包,npm uninstall axios element-ui vue vue-router vuex
- 刪除
main.js
裡element-ui
相關程式碼。
具體細節可以檢視 git
的歷史記錄
2.1 比對新增 CDN 前後構建的檔案:
優化後:

優化前:

可以看出:
-
app.css
: 因為不再通過import 'element-ui/lib/theme-chalk/index.css'
,而是直接通過CDN
連結的方式引入element-ui
樣式,使得檔案小到了bytes
級別,因為它現在僅包含少量的專案的css
。 -
app.js
:幾乎無變化,因為這裡面主要還是自己業務的程式碼。 -
vendor.js
:將5個依賴的js
全部轉為CDN
連結後,已經小到了不足1KB
,其實裡面已經沒有任何第三方庫了。 -
數字.js
和mainfest.js
:這些檔案本來就很小,變化幾乎可以忽略。
2.2 同樣,禁用瀏覽器快取,網速限定為 Fast 3G
下的 Network
圖(執行在本地的 nginx
伺服器上
可以看出相同的網路環境下,載入從原來的7秒多,提速到現在的3秒多,提升非常明顯。而且更重要的一點是原本的方式,所有 的 js
和 css
等靜態資源都是請求的我們自己的 nginx
伺服器,而現在大部分的靜態資源都請求的是第三方的 CDN
資源, 這不僅可以帶來速度上的提升,在高併發的時候,這無疑大大降低的自己伺服器的頻寬壓力,想象一下原來首屏900多KB的檔案 現在僅剩20KB是請求自己伺服器的!

三、 Gzip 優化
使用 Gzip
兩個明顯的好處,一是可以減少儲存空間,二是通過網路傳輸檔案時,可以減少傳輸的時間。
3.1 如何開啟 gzip
壓縮
開啟 gzip
的方式主要是通過修改伺服器配置,以 nginx
伺服器為例,下圖是,使用同一套程式碼,在僅改變伺服器的 gzip
開關狀態的情況下的 Network
對比圖
未開啟 gzip
壓縮:

開啟 gzip
壓縮:

開啟 gzip
壓縮後的響應頭

從上圖可以明顯看出開啟 gzip
前後,檔案大小有三四倍的差距,載入速度也從原來的7秒多,提升到3秒多
附上 nginx
的配置方式
http { gzip on; gzip_static on; gzip_min_length 1024; gzip_buffers 4 16k; gzip_comp_level 2; gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php application/vnd.ms-fontobject font/ttf font/opentype font/x-woff image/svg+xml; gzip_vary off; gzip_disable "MSIE [1-6]\."; } 複製程式碼
3.2 前端能為gzip做點什麼
我們都知道 config/index.js
裡有一個 productionGzip
的選項,那麼它是做什麼用的?我們嘗試執行 npm install --save-dev [email protected]
,並把 productionGzip
設定為 true
,重新 build
,放在 nginx
伺服器下,看看有什麼區別:


我們會發現構建之後的檔案多了一些 js.gz
和 css.gz
的檔案,而且 vendor.js
變得更小了,這其實是因為我們開啟了 nginx
的 gzip_static on;
選項, 如果 gzip_static
設定為 on
,那麼就會使用同名的 .gz
檔案,不會佔用伺服器的CPU資源去壓縮。
3.3 前端快速搭建基於 node
的 gzip
服務
無法搭建 nginx
環境的前端小夥伴也可以按如下步驟快速啟動一個帶 gzip
的 express
伺服器
npm i express compression serve.js
var express = require('express') var app = express() // 開啟gzip壓縮,如果你想關閉gzip,註釋掉下面兩行程式碼,重新執行`node server.js` var compression = require('compression') app.use(compression()) app.use(express.static('dist')) app.listen(3000,function () { console.log('server is runing on http://localhost:3000') }) 複製程式碼
- 執行
node server.js
下圖是 express
開啟 gzip
的響應頭:

四、 Prerender 預渲染
大家都是知道:常見的 Vue
單頁應用構建之後的 index.html
只是一個包含根節點的空白頁面,當所有需要的 js
載入完畢之後,才會開始解析並建立 vnode
,然後再渲染出真實的 DOM
。當這些 js
檔案過大而網速又很慢或者出現意料之外的報錯時,就會出現所謂的白屏,相信做 Vue
開發的小夥伴們一定都遇到過這種情況。而且單頁應用還有一個很大的弊端就是對 SEO
很不友好。那麼如何解決這些問題呢?—— SSR
當然是很好的解決的方案,但這也意為著一定的學習成本和運維成本,而如果你已經有了一個現成的 vue
單頁應用,轉向 SSR
也並不是一個無縫的過程。那麼 預渲染 就顯得更加合適了。只需要安裝一個 webpack
的外掛 + 一些簡單的 webpack
配置就可以解決上述的兩個問題。
4.1 如何將單頁應用轉為預渲染
- 你需要將
router
設為history
模式,並相應的調整伺服器配置,這並不複雜。 -
npm i prerender-spa-plugin --save-dev
- 在
build/webpack.prod.conf.js
下新增如下配置(沒有路由懶載入的情況)。
const PrerenderSPAPlugin = require('prerender-spa-plugin') ... new PrerenderSPAPlugin({ staticDir: config.build.assetsRoot, routes: [ '/', '/Contacts' ], // 需要預渲染的路由(視你的專案而定) minify: { collapseBooleanAttributes: true, collapseWhitespace: true, decodeEntities: true, keepClosingSlash: true, sortAttributes: true } }) 複製程式碼
- 將
config/index.js
裡build
中的assetsPublicPath
欄位設定為'/'
,這是因為當你使用預渲染時,路由元件會編譯成相應資料夾下的index.html
,它會依賴static
目錄下的檔案,而如果使用相對路徑則會導致依賴的路徑錯誤,這也要求預渲染的專案最好是放在網站的根目錄下(這個坑我已經在prerender-spa-plugin
倉庫提過ISSUE
了,不過藉助postProcess
,自己再寫一個正則表示式,也能實現,如果你有這方面的需求,可以參考下面 路由懶載入帶來的坑 )。 - 調整
main.js
new Vue({ router, store, render: h => h(App) }).$mount('#app', true) // https://ssr.vuejs.org/zh/guide/hydration.html 複製程式碼
執行 npm run build
,你會發現, dist
目錄和以往不太一樣,不僅多了與指定路由同名的資料夾而且 index.html
早已渲染好了靜態頁面。

4.2 效果如何?
和之前一樣,我們依然禁用快取,將網速限定為 Fast 3G
(執行在本地的 nginx
伺服器上)。可以看到,在 vendor.js
還沒有載入完畢的時候(大概有700多kB,此時只加載了200多kB),頁面已經完整的呈現出來了。事實上,只需要 index.html
和 app.css
載入完畢,頁面的靜態內容就可以很好的呈現了。預渲染對於這些有大量靜態內容的頁面,無疑是很好的選擇。

4.3 路由懶載入帶來的坑
如果你的專案沒有做路由懶載入,那麼你大可放心的按上面所說的去實踐了。但如果你的專案裡用了,你應該會看到 webpackJsonp is not defined
的報錯。這個因為 prerender-spa-plugin
渲染靜態頁面時,也會將類似於 <script src="/static/js/0.9231fc498af773fb2628.js" type="text/javascript" async charset="utf-8"></script>
這樣的非同步 script
標籤注入到生成的 html
的 head
標籤內。這會導致它先於 app.js
, vendor.js
, manifest.js
(位於 body
底部)執行。( async
只是不會阻塞後面的 DOM
解析,這並不意味這它最後執行)。而且當這些 js
載入完畢後,又會在 head
標籤重複建立這個非同步的 script
標籤。雖然這個報錯不會對程式造成影響,但是最好的方式,還是不要把這些非同步元件直接渲染到最終的 html
中。好在 prerender-spa-plugin
提供了 postProcess
選項,可以在真正生成 html
檔案之前做一次處理,這裡我使用一個簡單的正則表示式,將這些非同步的 script
標籤剔除。本分支已經使用了 路由懶載入 ,你可以直接檢視 git
歷史,比對檔案和 base
分支的變化來對你的專案進行相應調整。
postProcess (renderedRoute) { renderedRoute.html = renderedRoute.html.replace(/<script.*src=".*[0-9]+\.[0-9a-z]*\.js"><\/script>/,'') return renderedRoute } 複製程式碼
除了這種解決方案,還有兩種不推薦的解決方案:
- 索性不使用路由懶載入。
- 將
HtmlWebpackPlugin
的inject
欄位設定為'head'
,這樣app.js,vendor.js,manifest.js
就會插入到head
裡,並在非同步的script
標籤上面。 但由於普通的script
是同步的,在他們全部載入完畢之前,頁面是無法渲染的,也就違背了prerender
的初衷,而且你還需要對main.js
作如下修改,以確保Vue
在例項化的時候可以找到<div id="app"></div>
,並正確掛載。
const app = new Vue({ // ... }) document.addEventListener('DOMContentLoaded', function () { app.$mount('#app') }) 複製程式碼
總結
雖然官方的腳手架已經提供很多開箱即用的優化,比如 css
壓縮合並, js
壓縮與模組化,小圖片轉 base64
等等,但我們能做的還很多。我沒有提及程式碼級別的優化細節,也是希望給大家提供一些可實踐的方案。上述三種方案或多或少都會給你專案帶來一些收益。優化也是一門玄學,可研究的東西很多。也希望其他小夥伴可以在評論區提供寶貴意見,或者直接向我的這個專案 vue-optimization 的 base
分支提交 PR
,好的方案我會採納並整理。目前三種方案整合的最終結果我已經放在 master 分支下,你可以克隆下來並在此基礎上開發你的專案。