Vue SPA 專案webpack打包優化指南

gold_gold發表於2018-10-26

最近一個小夥伴問我他們公司的Vue後臺專案怎麼首次載入要十多秒太慢了,有什麼能優化的,於是乎我開啟了他們的網站,發現主要耗時在載入vendor.js檔案這個檔案高達2M,於是乎我就拿來他們的程式碼看看,進行了一番折騰。最終還是取得了不錯的效果。

優化思路

對於網頁效能,如何提升載入速度、等原理以及操作,在 修言 大佬 這本 《前端效能優化原理與實踐》 書中介紹的很詳細,有興趣的小夥伴可以去看看。

本文將主要從 webpack 打包的角度進行一些首屏載入速度的優化,以及打包速度的優化的實踐

優化成效

我選取的是一個用vue-cli2.0+版本構建的 Vue + Vuex + Vue-router + axios + elment-ui 的一個後臺系統專案進行測試,大概有20個非同步載入路由頁面。

我們將優化分成了3個主要的角度,每一個角度優化後進行速度打包速度的測試,打包構建花費的時間列在下面:

  1. 優化resolve.modules、配置裝載機的 include & exclude、使用webpack-parallel-uglify-plugin 壓縮程式碼

  2. 配置 externals 使庫檔案採用cdn載入

  3. 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

原理

  1. webpack 的 resolve.modules 是用來配置模組庫(即 node_modules)所在的位置。當 js 裡出現 import 'vue' 這樣不是相對、也不是絕對路徑的寫法時,它便會到 node_modules 目錄下去找。

  2. 在預設配置下,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

原理

  1. webpackloaders 裡的每個子項都可以有 include 和 exclude 屬性:
  • include:匯入的檔案將由載入程式轉換的路徑或檔案陣列(把要處理的目錄包括進來)
  • exclude:不能滿足的條件(排除不處理的目錄)
  1. 我們可以使用 include 更精確地指定要處理的目錄,這可以減少不必要的遍歷,從而減少效能損失。
  2. 同時使用 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 外掛來壓縮程式碼

原理

  1. 預設情況下 webpack 使用 UglifyJS 外掛進行程式碼壓縮,但由於其採用單執行緒壓縮,速度很慢。
  2. 我們可以改用 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 來加速程式碼構建

原理

  1. 由於執行在 Node.js 之上的 Webpack 是單執行緒模型的,所以 Webpack 需要處理的事情只能一件一件地做,不能多件事一起做。
  2. 而 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包構成圖
根據上圖所知 vendor.js Parsed 後為739kb,包主要包含了 像 VueVue-routerelment-ui等之類需要全域性引入的庫檔案。這些庫檔案都是一些不經常變動的問題,所以我們可以考慮把他們分離出來,用cdn的方式把框架庫引入。

原理:

利用 webpackexternals 屬性 。文件

官網的解釋 :防止 將某些 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-uiumd 模組名是 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 在重複構建中,並沒有改動程式碼,快取起了重要作用

Vue SPA 專案webpack打包優化指南
但是這個時候我們來看看 vendor 包Parsed 後只有 24KB 左右,框架檔案利用cdn加速,以及瀏覽器快取機制,可以顯著提升首頁的訪問速度。我們可以把檔案部署在伺服器上,開啟Chrome network檢視具體的載入用時。

缺點

  1. 此方法就沒辦法使用 vue-devtools 谷歌除錯工具了,畢竟直接用的線上的資源包。但是,根據環境做區分修改部分程式碼,就可以實現開發環境用的本地包,打包後的使用cdn資源。具體請參考這位大佬的實踐 Vue SPA 首屏載入優化實踐 ,可以區分環境來引入。
  2. 請求代價可能大於下載代價,在web優化指南中,就是儘量整合檔案,減小請求數量,這樣多了很多cdn資源並不一定合適。。

3.webpack DllPluginwebpack DllReferencePlugin 預編譯第三方庫檔案

既然 cdn 還是有他的弊端,那麼我們為何不考慮把庫檔案合併呢,所以我們利用 webpack.DllPlugin + webpack DllReferencePlugin + add-asset-html-webpack-plugin 預編譯並且引入

原理:

  1. 利用 webpack DllPlugin 外掛將第三方外掛單獨打包出來至 vendor.dll.js
  2. 利用 webpack DllReferencePlugin 是把這些預先編譯好的模組引用起來
  3. 利用 add-asset-html-webpack-pluginvendor.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 屬性內的相關庫檔案就打包在內了。

Vue SPA 專案webpack打包優化指南

Vue SPA 專案webpack打包優化指南

  • 開啟 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)

相關文章