阅读 4078

Vue SPA 项目webpack打包优化指南

最近一个小伙伴问我他们公司的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 在重复构建中,并没有改动代码,缓存起了重要作用

但是这个时候我们来看看 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)
关注下面的标签,发现更多相似文章
评论