最近一個小夥伴問我他們公司的Vue後臺專案怎麼首次載入要十多秒太慢了,有什麼能優化的,於是乎我開啟了他們的網站,發現主要耗時在載入vendor.js檔案這個檔案高達2M,於是乎我就拿來他們的程式碼看看,進行了一番折騰。最終還是取得了不錯的效果。
優化思路
對於網頁效能,如何提升載入速度、等原理以及操作,在 修言 大佬 這本 《前端效能優化原理與實踐》 書中介紹的很詳細,有興趣的小夥伴可以去看看。
本文將主要從
webpack
打包的角度進行一些首屏載入速度的優化,以及打包速度的優化的實踐
優化成效
我選取的是一個用vue-cli2.0+版本構建的 Vue
+ Vuex
+ Vue-router
+ axios
+ elment-ui
的一個後臺系統專案進行測試,大概有20個非同步載入路由頁面。
我們將優化分成了3個主要的角度,每一個角度優化後進行速度打包速度的測試,打包構建花費的時間列在下面:
-
優化resolve.modules、配置裝載機的 include & exclude、使用webpack-parallel-uglify-plugin 壓縮程式碼
-
配置 externals 使庫檔案採用cdn載入
-
webpack DllPlugin、webpack DllReferencePlugin 分離框架庫檔案
次數\打包耗時(s) | 原始配置用時 | 優化步驟1 | 優化步驟2 | 優化步驟3 |
---|---|---|---|---|
1 | 24.86 | ==23.86== | 11.22 | 13.92 |
2 | 23.52 | 14.51 | 11.04 | 12.63 |
3 | 25.49 | 14.04 | 11.29 | 13.19 |
4 | 24.84 | 14.56 | 11.25 | 13.14 |
5 | 24.60 | 15.44 | 11.86 | 14 |
由此可看出,還是能達到顯著的提升了10多s左右效果。具體時間,當然跟你的專案又關係。接下來,我們將介紹如何具體操作。
優化步驟
1. 通過基本的webpack外掛來加速打包
我們首先通過修改基本的
webpack
配置的方式提升打包速率
1.優化resolve.modules
原理:
-
webpack 的 resolve.modules 是用來配置模組庫(即 node_modules)所在的位置。當 js 裡出現 import 'vue' 這樣不是相對、也不是絕對路徑的寫法時,它便會到 node_modules 目錄下去找。
-
在預設配置下,webpack 會採用向上遞迴搜尋的方式去尋找。但通常專案目錄裡只有一個 node_modules,且是在專案根目錄。為了減少搜尋範圍,可我們以直接寫明 node_modules 的全路徑
所以平時在寫 import
匯入模組的時候引入指向的是具體的哪個檔案,也對打包速度的提升又一定的影響
操作:
開啟 build/webpack.base.conf.js
檔案,新增如下 modules
程式碼塊:
module.exports = {
resolve: {
...
modules: [
resolve('src'),
resolve('node_modules')
],
...
},
複製程式碼
2.配置loader的 include & exclude
原理:
webpack
的loaders
裡的每個子項都可以有 include 和 exclude 屬性:
- include:匯入的檔案將由載入程式轉換的路徑或檔案陣列(把要處理的目錄包括進來)
- exclude:不能滿足的條件(排除不處理的目錄)
- 我們可以使用 include 更精確地指定要處理的目錄,這可以減少不必要的遍歷,從而減少效能損失。
- 同時使用 exclude 對於已經明確知道的,不需要處理的目錄,予以排除,從而進一步提升效能。
操作:
開啟 build/webpack.base.conf.js
檔案,新增如下 include
,exclude
配置:
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig,
include: [resolve('src')], // 新增配置
exclude: /node_modules\/(?!(autotrack|dom-utils))|vendor\.dll\.js/ // 新增配置
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')], // 新增配置
exclude: /node_modules/ // 新增配置
},
複製程式碼
除此之外,如果我們選擇開啟快取將轉譯結果快取至檔案系統,則至少可以將 babel-loader 的工作效率提升兩倍。要做到這點,我們只需要為 loader 增加相應的引數設定:
loader: 'babel-loader?cacheDirectory=true'
複製程式碼
3.使用 webpack-parallel-uglify-plugin 外掛來壓縮程式碼
原理:
- 預設情況下
webpack
使用UglifyJS
外掛進行程式碼壓縮,但由於其採用單執行緒壓縮,速度很慢。 - 我們可以改用
webpack-parallel-uglify-plugin
外掛,它可以並行執行UglifyJS
外掛,從而更加充分、合理的使用 CPU 資源,從而大大減少構建時間,該外掛能設定快取,大大減小構建時間。
操作:
1.安裝 webpack-parallel-uglify-plugin
外掛
yarn add webpack-parallel-uglify-plugin -D
// or
npm i webpack-parallel-uglify-plugin -D
複製程式碼
2.開啟 build/webpack.prod.conf.js
檔案,並作如下修改
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
...
// 刪掉webpack提供的UglifyJS外掛
//new UglifyJsPlugin({
// uglifyOptions: {
// compress: {
// warnings: false
// }
// },
// sourceMap: config.build.productionSourceMap,
// parallel: true
//}),
// 增加 webpack-parallel-uglify-plugin來替換
new ParallelUglifyPlugin({
cacheDir: '.cache/',
uglifyJS:{
output: {
comments: false
},
compress: {
warnings: false,
drop_debugger: true, // 去除生產環境的 debugger 和 console.log
drop_console: true
}
}
}),
...
複製程式碼
使用 HappyPack 來加速程式碼構建
原理:
- 由於執行在 Node.js 之上的 Webpack 是單執行緒模型的,所以 Webpack 需要處理的事情只能一件一件地做,不能多件事一起做。
- 而 HappyPack 的處理思路是:將原有的 webpack 對 loader 的執行過程,從單一程式的形式擴充套件多程式模式,從而加速程式碼構建。
操作:
這一步具體操作,就沒貼程式碼了,我感覺沒作用不明顯,時間還加了一點點,可能是跟專案有關把,想使用的小夥伴自行百度用到自己專案裡面試試。
檢視效果
當你把上面這些優化都做完了,執行build的時候發現第一次所需要的構建時間跟最開始一樣23s左右,稍微少了2秒(主要是優化resolve,loader等的效果)
再次build的時候時間大大減少,因為在跟目錄下 .cache/
下快取了 Uglify
相關的js多以大大提高了構建的速度。趕緊去試試把。小夥伴們。
2. 配置 externals 使庫檔案採用cdn載入
開頭說到由於
vendor.js
過大引起的首頁載入慢,但是vue打包好的 vendor.js 是由什麼構成的呢?
vue-cli 生成的專案中 整合了 webpack-bundle-analyzer 依賴視覺化分析工具
執行
npm run build --report
複製程式碼
根據上圖所知 vendor.js
Parsed 後為739kb,包主要包含了 像 Vue
、Vue-router
、elment-ui
等之類需要全域性引入的庫檔案。這些庫檔案都是一些不經常變動的問題,所以我們可以考慮把他們分離出來,用cdn的方式把框架庫引入。
原理:
利用 webpack
的 externals
屬性 。文件
官網的解釋 :防止 將某些 import 的包(package) 打包 到 bundle 中,而是在執行時(runtime)再去從外部獲取這些擴充套件依賴(external dependencies)。
通俗的解釋:讓某些資源包即使不在本地npm安裝,通過 script
標籤引入後也能使用
操作:
- 首先在模板檔案
index.html
中新增以下內容
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>XXXX平臺</title>
<link rel="stylesheet" href="https://cdn.bootcss.com/element-ui/2.4.1/theme-chalk/index.css">
</head>
<body>
<div id="app"></div>
<script src="https://cdn.bootcss.com/vue/2.5.2/vue.min.js"></script>
<script src="https://cdn.bootcss.com/vuex/3.0.1/vuex.min.js"></script>
<script src="https://cdn.bootcss.com/vue-router/3.0.1/vue-router.min.js"></script>
<script src="https://cdn.bootcss.com/axios/0.17.0/axios.min.js"></script>
<script src="https://cdn.bootcss.com/element-ui/2.4.1/index.js"></script>
<!-- built files will be auto injected -->
</body>
</html>
複製程式碼
注意!版本號要與 package.json
中的版本號一致
- 修改
build/webpack.base.conf.js
module.exports = {
...
externals: {
'vue': 'Vue',
'vuex': 'Vuex',
'vue-router': 'VueRouter',
'axios': 'axios',
'element-ui': 'ELEMENT'
}
...
}
複製程式碼
注意!這裡 axios
變數名要使用 axios
注意!這裡 element-ui
變數名要使用 ELEMENT
,因為element-ui
的 umd
模組名是 ELEMENT
- 修改
src/router/index.js
// import Vue from 'vue'
import VueRouter from 'vue-router'
// 註釋掉
// Vue.use(VueRouter)
...
}
複製程式碼
- 修改
src/store/index.js
...
// 註釋掉
// Vue.use(Vuex)
...
}
複製程式碼
- 修改
src/main.js
import Vue from 'vue'
import App from './App.vue'
import ElementUI from 'element-ui'
// 註釋掉
// import 'element-ui/lib/theme-chalk/index.css'
// router setup
import router from './router'
// Vuex setup
import store from './store'
Vue.use(ElementUI)
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
store,
template: '<App/>',
components: { App }
})
複製程式碼
完事
上面都配置好了後啟動 npm run build
發現構建時間在11-12s左右,為什麼相比較於步驟1的提升並不大呢,因為步驟1中 ParallelUglifyPlugin
在重複構建中,並沒有改動程式碼,快取起了重要作用
vendor
包Parsed 後只有 24KB
左右,框架檔案利用cdn加速,以及瀏覽器快取機制,可以顯著提升首頁的訪問速度。我們可以把檔案部署在伺服器上,開啟Chrome network檢視具體的載入用時。
缺點
- 此方法就沒辦法使用
vue-devtools
谷歌除錯工具了,畢竟直接用的線上的資源包。但是,根據環境做區分修改部分程式碼,就可以實現開發環境用的本地包,打包後的使用cdn資源。具體請參考這位大佬的實踐 Vue SPA 首屏載入優化實踐 ,可以區分環境來引入。 - 請求代價可能大於下載代價,在web優化指南中,就是儘量整合檔案,減小請求數量,這樣多了很多cdn資源並不一定合適。。
3.webpack DllPlugin
、webpack DllReferencePlugin
預編譯第三方庫檔案
既然 cdn 還是有他的弊端,那麼我們為何不考慮把庫檔案合併呢,所以我們利用
webpack.DllPlugin
+webpack DllReferencePlugin
+add-asset-html-webpack-plugin
預編譯並且引入
原理:
- 利用
webpack DllPlugin
外掛將第三方外掛單獨打包出來至vendor.dll.js
- 利用
webpack DllReferencePlugin
是把這些預先編譯好的模組引用起來 - 利用
add-asset-html-webpack-plugin
把vendor.dll.js
包插入html
操作:
我們還是從操作1完成後繼續修改程式碼(cdn的相關操作程式碼退回)
- 在
build
資料夾中新建webpack.dll.conf.js
檔案,內容如下(主要是配置下需要提前編譯打包的庫):
var path = require('path')
var webpack = require('webpack')
var context = path.join(__dirname, '..')
module.exports = {
entry: {
vendor: [
'vue/dist/vue.common.js',
'vuex',
'vue-router',
'axios',
'element-ui'
]
},
output: {
path: path.join(context, 'static/js'), // 打包後的 vendor.js放入 static/js 路徑下
filename: '[name].dll.js',
library: '[name]'
},
resolve: {
alias: {
'vue$': 'vue/dist/vue.esm.js'
}
},
plugins: [
new webpack.DllPlugin({
path: path.join(context, '[name].manifest.json'),
name: '[name]',
context: context
}),
// 壓縮js程式碼
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
},
output: { // 刪除打包後的註釋
comments: false
}
})
]
}
複製程式碼
- 編輯
package.json
檔案,新增一條編譯命令:
"scripts": {
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
"start": "npm run dev",
"lint": "eslint --ext .js,.vue src",
"build": "node build/build.js",
"build:dll": "webpack --config build/webpack.dll.conf.js --progress"
},
複製程式碼
然後命令列執行 npm run build:dll
這時,會在 static/js 裡面生成 vendor.dll.js
, vendor
屬性內的相關庫檔案就打包在內了。
- 開啟
index.html
這邊將vendor.dll.js
引入進來。
<body>
<div id="app"></div>
<script src="./static/js/vendor.dll.js"></script>
</body>
複製程式碼
- 開啟
build/webpack.base.conf.js
檔案,編輯新增如下配置,作用是通過 DLLReferencePlugin 來使用 DllPlugin 生成的 DLL Bundle
const webpack = require('webpack');
module.exports = {
...
plugins: [
new webpack.DllReferencePlugin({
// name引數和dllplugin裡面name一致,可以不傳
name: 'vendor',
// dllplugin 打包輸出的manifest.json
manifest: require('../vendor.manifest.json'),
// 和dllplugin裡面的context一致
context: path.join(__dirname, '..')
})
]
...
}
複製程式碼
- 修改
build/webpack.prod.js
註釋掉CommonsChunkPlugin
相關程式碼,因為庫檔案在之前的 vendor.dll.js 中已經編譯好了,不需要在編譯
module.exports = {
plugins: [
...
// 去掉這裡的CommonsChunkPlugin
// new webpack.optimize.CommonsChunkPlugin({
// name: 'vendor',
// minChunks (module) {
// // any required modules inside node_modules are extracted to vendor
// return (
// module.resource &&
// /\.js$/.test(module.resource) &&
// module.resource.indexOf(
// path.join(__dirname, '../node_modules')
// ) === 0
// )
// }
// }),
// 去掉這裡的CommonsChunkPlugin
// new webpack.optimize.CommonsChunkPlugin({
// name: 'manifest',
// minChunks: Infinity
// }),
...
]
}
複製程式碼
完事
至此,儲存程式碼,進行構建,發現構建時間大概在14s左右。怎麼比cdn時間還增多了呢,因為element-ui的樣式檔案還需要每次打包,樣式不建議單獨打包出來,要麼也是使用cdn的方式。
最後我們還是部署到伺服器上開啟Chrome network檢視網頁具體的載入用時。
開啟構建依賴圖,發現vendor
檔案已經不見了,不需要每次打包了,直接引入vendor.dll.js
檔案就好,這樣還有一個好處:當你有多個專案的依賴相同的時候,引用同一份dll
即可。
真的就完事兒了?
大家有沒有注意到 vendor.dll.js
是一個固定的檔案,沒有加 hash 字尾,這對快取來說是致命的,當你升級了庫或者增加了庫檔案,重新打包後的 還是叫做 vendor.dll.js
檔案,沒有破壞快取,當使用者訪問時程式可能會出現問題。
有時候開發環境和測試環境可能 引入的vendor.dll.js
路徑不一樣你得手動更改,也是一個問題。既然這樣怎麼辦呢??
還好有 add-asset-html-webpack-plugin
這個外掛進行依賴資源的注入,本人在實踐的時候以為找到了救命稻草。可是奈何不知道是姿勢不對,還是該外掛已經過時未升級,程式執行時候報錯,無法使用,也希望使用過的大佬,指點一下。。
結語
至此關於 Vue SPA 專案中的優化,介紹的差不多了,但是僅僅只是提供一個思路,優化並不是一成不變的,有些專案可能只需要步驟1,有些專案可能引用資源小採用cdn的方式也可以,而有些多個專案依賴都相同,就可考慮dll,當然是根據具體的場景來進行選擇優化。
最終還是以部署到伺服器後,清除快取訪問,後分析載入時間。畢竟載入時間比打包時間重要得多
但是,我們平時寫程式碼的時候應該多多思考,在寫程式碼的時候注意一些細節,也能提升不少效率和效能。
舉個例子1:很多專案會用到 echarts
,我發現有小夥伴把 echarts
注入在 main.js
中,這顯然是沒必要的白白增大了 vendor.js
的大小,應該在僅僅需要使用的頁面去引入就好,還得注意echarts
的地圖元件,是採用同步渲染,還是非同步渲染好呢,還有根據視窗的 resize
,是否注意防抖和節流呢。
舉個例子2:當我們使用百度地圖的jssdk的時候,是在 index.html
裡面通過 script
標籤引入,還是在某個頁面需要使用地圖的時候採用非同步載入的形式呢。這些都是值得我們思考的問題。
所以從每一步寫程式碼的細節多多思考。
至此寫完了,我也是抱著學習的態度,如有什麼錯誤,請大佬們斧正,順便請教 add-asset-html-webpack-plugin
的正確姿勢。
附錄
相關程式碼託管在github vue-spa-optimization 上,上面有4個分支
master:
:未做任何優化的原始版本simple:
做了上面步驟1中相關優化的版本cdn:
做了上面步驟1與步驟2優化的版本(cdn)dll:
做了上面步驟1與步驟3優化的版本(dll)