Vue打包優化之code spliting

林淙源tinylin發表於2018-04-07

前言

在http1的時代,比較常見的一種效能優化就是合併http的請求數量,通常我們會把許多js程式碼合併在一起,但是如果一個js包體積特別大的話對於效能提升來說就有點矯枉過正了。而如果我們對所有的程式碼進行合理的拆分,將首屏和非首屏的程式碼進行剝離,將業務程式碼和基礎庫程式碼進行拆分,在需要某段程式碼的時候再載入它,下次若再需要用則從快取中讀取,一來可以更好地使用瀏覽器快取,再者就是可以提高首屏載入速度,很好提升使用者的體驗。

核心思想

業務程式碼和基礎庫的分離

這個其實很好理解,業務程式碼通常更新迭代很頻繁,而基礎庫通常更新緩慢,這裡做拆分的話可以充分利用瀏覽器快取來載入基礎庫程式碼。

按需非同步載入

這個主要解決首屏請求大小的問題,我們在訪問首屏的時候只需要載入首屏所需的邏輯,而不是載入所有路由的程式碼。

實戰

最近,採用vuetify改造了一個內部系統,一開始用了最常用的webpack配置,功能很快開發了,可是一打包,發現效果不是很明顯,打出很多大包

Vue打包優化之code spliting

這裡我們看下打包分佈,這裡使用的是 webpack-bundle-analyzer,可以很清晰的看到 vue 和 vuetify等模組都有出現 被重複打包的情況。

Vue打包優化之code spliting

這裡我們先貼一下配置,一邊一會兒分析時用:

const path = require('path')
const webpack = require('webpack')
const CleanWebpackPlugin = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

const generateHtml = new HtmlWebpackPlugin({
  title: '逍遙系統',
  template: './src/index.html',
  minify: {
    removeComments: true
  }
})

module.exports = {
  entry: {
    vendor: ['vue', 'vue-router', 'vuetify'],
    app: './src/main.js'
  },
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: '[name].[hash].js',
    chunkFilename:'[id].[name].[chunkhash].js'
  },
  resolve: {
    extensions: ['.js', '.vue'],
    alias: {
      'vue$': 'vue/dist/vue.esm.js',
      'public': path.resolve(__dirname, './public')
    }
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: {
          loaders: {
          }
          // other vue-loader options go here
        }
      },
      {
        test: /\.js$/,
        loader: 'babel-loader',
        exclude: /node_modules/
      },
      {
        test: /\.(png|jpg|gif|svg)$/,
        loader: 'file-loader',
        options: {
          objectAssign: 'Object.assign'
        }
      },
      {
        test: /\.css$/,
        loader: ['style-loader', 'css-loader']
      },
      {
        test: /\.styl$/,
        loader: ['style-loader', 'css-loader', 'stylus-loader']
      }
    ]
  },
  devServer: {
    historyApiFallback: true,
    noInfo: true
  },
  performance: {
    hints: false
  },
  devtool: '#eval-source-map',
  plugins: [
      new BundleAnalyzerPlugin(),
      new CleanWebpackPlugin(['dist']),
      generateHtml,
      new webpack.optimize.CommonsChunkPlugin({
        name: 'ventor'
      }),
  ]
}

if (process.env.NODE_ENV === 'production') {
  module.exports.devtool = '#source-map'
  // http://vue-loader.vuejs.org/en/workflow/production.html
  module.exports.plugins = (module.exports.plugins || []).concat([
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: '"production"'
      }
    }),
    new webpack.optimize.UglifyJsPlugin({
      sourceMap: true,
      compress: {
        warnings: false
      }
    }),
    new webpack.LoaderOptionsPlugin({
      minimize: true
    })
  ])
}

複製程式碼

CommonChunkPlugin

ventor入口這裡我們發現並沒有篩選出所有引用的node_module下的模組 ,比如axios ,所以導致打包到了app.js裡了,這裡我們做下分離

entry: {
    vendor: ['vue', 'vue-router', 'vuetify', 'axios'],
    app: './src/main.js'
  },
複製程式碼

那這裡又出現個問題了,我不可能手動去手動錄入模組,這時我們可能需要 自動化分離 ventor,這裡我們需要引入 minChunks,在配置中我們就可以對所有mode_module下所引用的模組進行打包 修改配置如下

entry: {
    //vendor: ['vue', 'vue-router', 'vuetify', 'axios'], //刪除
    app: './src/main.js'
  }

new webpack.optimize.CommonsChunkPlugin({
        name: 'vendor',
        minChunks: ({ resource }) => (
          resource &&
          resource.indexOf('node_modules') >= 0 &&
          resource.match(/\.js$/)
        )
 }),
複製程式碼

經過上面幾步的優化,我們再看看檔案分佈,會發現node_module下的模組都收歸到了vendor下了。

Vue打包優化之code spliting
Vue打包優化之code spliting

這裡我們可以得到一個經驗,就是在一個專案中可以專門針對node_module下的模組進行打包優化。但是這裡細心的你可能發現codemirror元件不也是node_module中的麼,但為啥沒被打包進去反而重複打包到其他單頁面了呢,其實這裡是因為在commonChunk中使用name屬性其實也就意味著只會沿著entry入口去找尋所依賴的包,由於我們的元件採用的是非同步載入,故這裡就不會去打包了,我們做個實驗驗證下,現在我們去掉dbmanage和system頁面的路由懶載入改為直接引入

// const dbmanage = () => import(/* webpackChunkName: "dbmanage" */'../views/dbmanage.vue')
// const system = () => import(/* webpackChunkName: "system" */'../views/system.vue')
import dbmanage from '../views/dbmanage.vue'
import system from '../views/system.vue'
複製程式碼

這時我們重新打包可以發現,codemirror被打包進來了,那麼問題來了,這樣子好麼?

Vue打包優化之code spliting

async

上面的問題答案是肯定的,不可以的,很明顯ventor是我們的入口程式碼即首屏,我們完全沒有必要去載入這個codemirror元件,我們先把剛才的路由修改恢復回去,但是這時又有了新問題,我們的codemirror被同時打包進了兩個單頁面,並且還有些自己封裝的components,例如MTable或是MDataTable等也出現了重複打包。並且codemirror特別大,同時載入到兩個單頁面也會造成很大的效能問題,簡單說就是,我們在訪問第一個單頁面載入了codemirror之後,在第二個頁面其實就不應該再載入了。 要解決這個問題,這裡我們可以使用 CommonsChunkPlugin 的 async 並在 minChunnks 裡的count方法來判斷數量,只要是 重用次數 超過兩個包括兩個的非同步載入模組(即 import () 產生的chunk )我們都認為是 可以 打成公共的 ,這裡我們增加一項配置。

new webpack.optimize.CommonsChunkPlugin({
  async: 'used-twice',
  minChunks: (module, count) => (
    count >= 2
  ),
})
複製程式碼

再次打包,我們發現所有服用的元件被重新打到了 0.used-twice-app.js中了,這樣各個單頁面大小也有所下降,平均小了近10k左右

Vue打包優化之code spliting

Vue打包優化之code spliting

可是,這裡我們發現vuetify.js和vuetify.css實在太龐大了,導致我們的打包的程式碼很大,這裡,我們考慮把它提取出來,這裡為了避免重複打包,需要使用external,並將vue以及vuetify的程式碼採用cdn讀取的方式,首先修改index.html

css引入
<link href='https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons' rel="stylesheet" type="text/css">
<link href="https://unpkg.com/vuetify/dist/vuetify.min.css" rel="stylesheet">
js引入
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vuetify/dist/vuetify.js"></script>

//去掉main.js中之前對vuetifycss的引入
//import 'vuetify/dist/vuetify.css'
複製程式碼

再修改webpack配置,新增externals

externals: {
    'vue':'Vue',
    "vuetify":"Vuetify"
  }
複製程式碼

再重新打包,可以看到vue相關的程式碼已經沒有了,目前也只有used-twice-app.js比較大了,app.js縮小了近200kb。

Vue打包優化之code spliting

Vue打包優化之code spliting

但是新問題又來了,codemirror很大,而used-twice又是首屏需要的,這個打包在首屏肯定不是很好,這裡我們要將system和dbmanage頁面的codemirror元件改為非同步載入,單獨打包,修改如下:

// import MCode from "../component/MCode.vue"; //註釋掉

components: {
      MDialog,
      MCode: () => import(/* webpackChunkName: "MCode" */'../component/MCode.vue')
 },
複製程式碼

重新打包下,可以看到 codemirror被抽離了,首屏程式碼進一步得到了減少,used-twice-app.js程式碼縮小了近150k。

Vue打包優化之code spliting

Vue打包優化之code spliting

做了上面這麼多的優化之後,業務測的js基本都被拆到了50kb一下(忽略map檔案),算是優化成功了。

總結

可能會有朋友會問,單獨分拆vue和vuetify會導致請求數增加,這裡我想補充下,我們的業務現在已經切換成http2了,由於多路複用,並且加上瀏覽器快取,我們分拆出的請求數其實也算是控制在合理的範疇內。

這裡最後貼一下優化後的webpack配置,大家一起交流學習下哈。

const path = require('path')
const webpack = require('webpack')
const CleanWebpackPlugin = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

const generateHtml = new HtmlWebpackPlugin({
  title: '逍遙系統',
  template: './src/index.html',
  minify: {
    removeComments: true
  }
})

module.exports = {
  entry: {
    app: './src/main.js'
  },
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: '[name].[hash].js',
    chunkFilename:'[id].[name].[chunkhash].js'
  },
  resolve: {
    extensions: ['.js', '.vue'],
    alias: {
      'vue$': 'vue/dist/vue.esm.js',
      'public': path.resolve(__dirname, './public')
    }
  },
  externals: {
    'vue':'Vue',
    "vuetify":"Vuetify"
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: {
          loaders: {
          }
          // other vue-loader options go here
        }
      },
      {
        test: /\.js$/,
        loader: 'babel-loader',
        exclude: /node_modules/
      },
      {
        test: /\.(png|jpg|gif|svg)$/,
        loader: 'file-loader',
        options: {
          objectAssign: 'Object.assign'
        }
      },
      {
        test: /\.css$/,
        loader: ['style-loader', 'css-loader']
      },
      {
        test: /\.styl$/,
        loader: ['style-loader', 'css-loader', 'stylus-loader']
      }
    ]
  },
  devServer: {
    historyApiFallback: true,
    noInfo: true
  },
  performance: {
    hints: false
  },
  devtool: '#eval-source-map',
  plugins: [
      new CleanWebpackPlugin(['dist']),
      generateHtml
  ]
}

if (process.env.NODE_ENV === 'production') {
  module.exports.devtool = '#source-map'
  module.exports.plugins = (module.exports.plugins || []).concat([
    new BundleAnalyzerPlugin(),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'ventor',
      minChunks: ({ resource }) => (
        resource &&
        resource.indexOf('node_modules') >= 0 &&
        resource.match(/\.js$/)
      )
    }),

    new webpack.optimize.CommonsChunkPlugin({
      async: 'used-twice',
      minChunks: (module, count) => (
        count >= 2
      ),
    }),

    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: '"production"'
      }
    }),
    new webpack.optimize.UglifyJsPlugin({
      sourceMap: true,
      compress: {
        warnings: false
      }
    }),
    new webpack.LoaderOptionsPlugin({
      minimize: true
    })
  ])
}

複製程式碼

參考資料:

  • Webpack 大法之 Code Splitting:https://zhuanlan.zhihu.com/p/26710831
  • vue+webpack實現非同步元件載入:http://blog.csdn.net/weixin_36094484/article/details/74555017
  • VUE2元件懶載入淺析:https://www.cnblogs.com/zhanyishu/p/6587571.html

下面是我們QQ音樂前端團隊公眾號,希望大家支援支援哈,我們會努力寫出好的文章分享給大家

Vue打包優化之code spliting

相關文章