本文永久連接:github.com/HaoChuan942…javascript
這些優化方案適用於
Vue CLI 2
和Vue CLI 3
,文章主要基於Vue CLI 2
進行介紹,關於如何在Vue CLI 3
中進行相關的webpack
調整,我已經放在了 vue-cli3-optimization 這個倉庫下,並配有詳細的註釋,且額外添加方便Sass
使用的loader
,使用Sass
時無需再在每一個須要引入變量和mixin
的地方,每次都很麻煩的@import
。下面將詳細介紹這些優化方案的實踐方式和效果:php
和不少小夥伴同樣,我在開發Vue
項目時也是基於官方vue-cli@2
的webpack
模版,但隨着項目越作越大,依賴的第三方npm
包愈來愈多,構建以後的文件也會愈來愈大,尤爲是vendor.js
,甚至會達到2M
左右。再加上又是單頁應用,這就會致使在網速較慢或者服務器帶寬有限的狀況出現長時間的白屏。爲了解決這個問題,我作了一些探索,在幾乎不須要改動業務代碼的狀況下,找到了三種有明顯效果的優化方案 —— CDN
+ Gzip
+ Prerender
。我把這些方法整理了一下,放在了 Github倉庫 上,意圖經過不一樣的分支來展現不一樣的優化方式,對Vue
項目性能的影響。你能夠直接克隆下來試一試,也得益於有git
歷史,你也能夠很方便的查看具體的改動細節。下面我將經過一個簡單的項目來展現這三種優化方案的效果。css
經過vue-cli@2
的webpack
模版生成,只包含最基礎的Vue
三件套 ———— vue
、vue-router
、vuex
以及經常使用的element-ui
和axios
。拆分兩個路由——「首頁」和「通信錄」,經過axios
異步獲取一個通信錄名單,並利用element-ui
的表格展現。直接build
,不作任何優化處理,以做參照。html
app.css
: 壓縮合並後的樣式文件。app.js
:主要包含項目中的App.vue
、main.js
、router
、store
等業務代碼。vendor.js
:主要包含項目依賴的諸如vuex
,axios
等第三方庫的源碼,這也是爲何這個文件如此之大的緣由,下一步將探索如何優化這一塊,畢竟隨着項目的開發,依賴的庫也能會愈來愈多。數字.js
:以0、一、二、3等數字開頭的js
文件,這些文件是各個路由切分出的代碼塊,由於我拆分了兩個路由,並作了路由懶加載,因此出現了0和1兩個js
文件。mainfest.js
:mainfest
的英文有清單、名單的意思,該文件包含了加載和處理路由模塊的邏輯Fast 3G
下的Network
圖(運行在本地的nginx
服務器上能夠看到未經優化的base
版本在Fast 3G
的網絡下大概須要7秒多的時間才加載完畢前端
爲了更好的開發體驗,報錯捕獲,目前已經針對
dev
和build
進行了區分,具體查看git
記錄,下面僅供參考。vue
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
的歷史記錄java
優化後: node
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
:這些文件原本就很小,變化幾乎能夠忽略。Fast 3G
下的Network
圖(運行在本地的nginx
服務器上能夠看出相同的網絡環境下,加載從原來的7秒多,提速到如今的3秒多,提高很是明顯。並且更重要的一點是本來的方式,全部 的js
和css
等靜態資源都是請求的咱們本身的nginx
服務器,而如今大部分的靜態資源都請求的是第三方的CDN
資源, 這不只能夠帶來速度上的提高,在高併發的時候,這無疑大大下降的本身服務器的帶寬壓力,想象一下原來首屏900多KB的文件 如今僅剩20KB是請求本身服務器的! webpack
使用Gzip
兩個明顯的好處,一是能夠減小存儲空間,二是經過網絡傳輸文件時,能夠減小傳輸的時間。ios
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]\.";
}
複製代碼
咱們都知道config/index.js
裏有一個productionGzip
的選項,那麼它是作什麼用的?咱們嘗試執行npm install --save-dev compression-webpack-plugin@1.x
,並把productionGzip
設置爲true
,從新build
,放在nginx
服務器下,看看有什麼區別:
咱們會發現構建以後的文件多了一些js.gz
和css.gz
的文件,並且vendor.js
變得更小了,這實際上是由於咱們開啓了nginx
的gzip_static on;
選項, 若是gzip_static
設置爲on
,那麼就會使用同名的.gz
文件,不會佔用服務器的CPU資源去壓縮。
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
配置就能夠解決上述的兩個問題。
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
早已渲染好了靜態頁面。
和以前同樣,咱們依然禁用緩存,將網速限定爲Fast 3G
(運行在本地的nginx
服務器上)。能夠看到,在vendor.js
尚未加載完畢的時候(大概有700多kB,此時只加載了200多kB),頁面已經完整的呈現出來了。事實上,只須要index.html
和app.css
加載完畢,頁面的靜態內容就能夠很好的呈現了。預渲染對於這些有大量靜態內容的頁面,無疑是很好的選擇。
若是你的項目沒有作路由懶加載,那麼你大可放心的按上面所說的去實踐了。但若是你的項目裏用了,你應該會看到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 分支下,你能夠克隆下來並在此基礎上開發你的項目。