Vue SPA 項目webpack打包優化指南

最近一個小夥伴問我他們公司的Vue後臺項目怎麼首次加載要十多秒太慢了,有什麼能優化的,因而乎我打開了他們的網站,發現主要耗時在加載vendor.js文件這個文件高達2M,因而乎我就拿來他們的代碼看看,進行了一番折騰。最終仍是取得了不錯的效果。javascript

優化思路

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

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

優化成效

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

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

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

  2. 配置 externals 使庫文件採用cdn加載node

  3. webpack DllPlugin、webpack DllReferencePlugin 分離框架庫文件webpack

次數\打包耗時(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左右效果。具體時間,固然跟你的項目又關係。接下來,咱們將介紹如何具體操做。ios

優化步驟

1. 經過基本的webpack插件來加速打包

咱們首先經過修改基本的 webpack 配置的方式提高打包速率git

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 在重複構建中,並無改動代碼,緩存起了重要做用

可是這個時候咱們來看看 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 屬性內的相關庫文件就打包在內了。

  • 打開 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)
相關文章
相關標籤/搜索