Vue CLI 2&3 下的專案優化實踐 —— CDN + Gzip + Prerender
前言
這些優化方案適用於
Vue CLI 2
和Vue CLI 3
,文章主要基於Vue CLI 2
進行介紹,關於如何在Vue CLI 3
中進行相關的webpack
調整,我已經放在了 vue-cli3-optimization 這個倉庫下,並配有詳細的註釋,且額外新增方便Sass
使用的loader
,使用Sass
時無需再在每個需要引入變數和mixin
的地方,每次都很麻煩的@import
。下面將詳細介紹這些優化方案的實踐方式和效果:
和很多小夥伴一樣,我在開發Vue
專案時也是基於官方[email protected]
的webpack
模版,但隨著專案越做越大,依賴的第三方npm
包越來越多,構建之後的檔案也會越來越大,尤其是vendor.js
2M
左右。再加上又是單頁應用,這就會導致在網速較慢或者伺服器頻寬有限的情況出現長時間的白屏。為了解決這個問題,我做了一些探索,在幾乎不需要改動業務程式碼的情況下,找到了三種有明顯效果的優化方案 —— CDN
+ Gzip
+ Prerender
。我把這些方法整理了一下,放在了 Github倉庫 上,意圖通過不同的分支來展示不同的優化方式,對Vue
專案效能的影響。你可以直接克隆下來試一試,也得益於有git
歷史,你也可以很方便的檢視具體的改動細節。下面我將通過一個簡單的專案來展示這三種優化方案的效果。
一、首先準備一個簡單的專案
通過[email protected]
的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 優化
為了更好的開發體驗,報錯捕獲,目前已經針對
dev
和build
進行了區分,具體檢視git
記錄,下面僅供參考。
- 將依賴的
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
兩個明顯的好處,一是可以減少儲存空間,二是通過網路傳輸檔案時,可以減少傳輸的時間。
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
的響應頭:
大家都是知道:常見的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
注意!!!預渲染需要下載
Chromium
,而由於你懂的原因,谷歌的東西在國內無法下載,所以在根目錄添加了.npmrc
檔案,來使用淘寶映象下載。參考連結。如果你的終端可以翻到國外,直接忽略這一步,你也許會喜歡小飛機
- 在
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 = renderedRoute.html.replace(/<script[^<]*src="[^<]*[0-9]+\.[0-9a-z]{20}\.js"><\/script>/g,function (target) {
console.log(chalk.bgRed('\n\n剔除的懶載入標籤:'), chalk.magenta(target))
return ''
})
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 分支下,你可以克隆下來並在此基礎上開發你的專案。