Vue打包优化之code spliting

前言

在http1的时代,比较常见的一种性能优化就是合并http的请求数量,一般咱们会把许多js代码合并在一块儿,可是若是一个js包体积特别大的话对于性能提高来讲就有点矫枉过正了。而若是咱们对全部的代码进行合理的拆分,将首屏和非首屏的代码进行剥离,将业务代码和基础库代码进行拆分,在须要某段代码的时候再加载它,下次若再须要用则从缓存中读取,一来能够更好地使用浏览器缓存,再者就是能够提升首屏加载速度,很好提高用户的体验。javascript

核心思想

业务代码和基础库的分离

这个其实很好理解,业务代码一般更新迭代很频繁,而基础库一般更新缓慢,这里作拆分的话能够充分利用浏览器缓存来加载基础库代码。css

按需异步加载

这个主要解决首屏请求大小的问题,咱们在访问首屏的时候只须要加载首屏所需的逻辑,而不是加载全部路由的代码。html

实战

最近,采用vuetify改造了一个内部系统,一开始用了最经常使用的webpack配置,功能很快开发了,但是一打包,发现效果不是很明显,打出不少大包前端

这里咱们看下打包分布,这里使用的是 webpack-bundle-analyzer,能够很清晰的看到 vue 和 vuetify等模块都有出现 被重复打包的状况。vue

这里咱们先贴一下配置,一边一下子分析时用:java

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里了,这里咱们作下分离node

entry: {
    vendor: ['vue', 'vue-router', 'vuetify', 'axios'],
    app: './src/main.js'
  },
复制代码

那这里又出现个问题了,我不可能手动去手动录入模块,这时咱们可能须要 自动化分离 ventor,这里咱们须要引入 minChunks,在配置中咱们就能够对全部mode_module下所引用的模块进行打包 修改配置以下webpack

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下了。ios

这里咱们能够获得一个经验,就是在一个项目中能够专门针对node_module下的模块进行打包优化。可是这里细心的你可能发现codemirror组件不也是node_module中的么,但为啥没被打包进去反而重复打包到其余单页面了呢,其实这里是由于在commonChunk中使用name属性其实也就意味着只会沿着entry入口去找寻所依赖的包,因为咱们的组件采用的是异步加载,故这里就不会去打包了,咱们作个实验验证下,如今咱们去掉dbmanage和system页面的路由懒加载改成直接引入web

// 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被打包进来了,那么问题来了,这样子好么?

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左右

但是,这里咱们发现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。

可是新问题又来了,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。

作了上面这么多的优化以后,业务测的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音乐前端团队公众号,但愿你们支持支持哈,咱们会努力写出好的文章分享给你们

相关文章
相关标签/搜索