# Vue 项目优化实践-webpack 篇

# 体积分析

经过webpack 打包后的体积优化是一个很重要的点,比如引入的第三方库是否过大,能否对体积过大的库进行优化。此时需要用到一款插件,叫做webpack-bundle-analyzer 。它可以用交互式可缩放树形图显示webpack输出文件的大小,用起来非常的方便。

首先安装插件:

npm install --save-dev webpack-bundle-analyzer
1

然后在vue.config.js 中引入:

const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
  .BundleAnalyzerPlugin;

module.exports = {
  plugins: [new BundleAnalyzerPlugin()],
};
1
2
3
4
5
6

然后npm run serve 启动项目,此时会默认启动 http://127.0.0.1:8888,页面里可以清晰的查看包占比的大小。建议只在开发环境启用,生产环境采用默认配置会打包失败。以tieshangongzhu 项目为例,打包结果中占比比较大的第三方库包括:iview.jsmoment.jslodash.js 等。下面介绍如何优化这些大的资源。

# 体积优化

这里介绍支持按需引入的babel 插件babel-plugin-import ,用来优化lodash

首先安装插件:

npm install babel-plugin-import --save-dev
1

然后在babel.config.js 中的plugins 数组中添加一下配置:

["import", { libraryName: "lodash", libraryDirectory: "" }];
1

通过上述配置就完成了lodash 的按需加载。

接着我们来优化moment ,通过分析页面查看可知,moment 很大部分占比是语言包,但我们基本用不到,于是我们可以借助webpack自带的插件来忽略语言包。配置如下:

plugins: [new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)];
1

通过上述配置即可去除语言包,减少大概 70%的大小。

# 多进程构建

大家都知道  webpack  是运行在  node  环境中,而  node  是单线程的。webpack  的打包过程是  io  密集和计算密集型的操作,如果能同时  fork  多个进程并行处理各个任务,将会有效的缩短构建时间。

这里采用thread-loader 进行多进程构建。

首先安装loader

npm install --save-dev thread-loader
1

然后在vue.config.js 添加如下配置:

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: ["thread-loader", "babel-loader"],
      },
    ],
  },
};
1
2
3
4
5
6
7
8
9
10
11

然后执行npm run build ,统计打包时长。通过对比会发现,引入前打包耗时 37s,引入后打包耗时 18s。速度有了一倍的提升。通过查看可选项配置:

options: {
      // the number of spawned workers, defaults to (number of cpus - 1) or
      // fallback to 1 when require('os').cpus() is undefined
      workers: 2,

      // number of jobs a worker processes in parallel
      // defaults to 20
      workerParallelJobs: 50,

      // additional node.js arguments
      workerNodeArgs: ['--max-old-space-size=1024'],

      // Allow to respawn a dead worker pool
      // respawning slows down the entire compilation
      // and should be set to false for development
      poolRespawn: false,

      // timeout for killing the worker processes when idle
      // defaults to 500 (ms)
      // can be set to Infinity for watching builds to keep workers alive
      poolTimeout: 2000,

      // number of jobs the poll distributes to the workers
      // defaults to 200
      // decrease of less efficient but more fair distribution
      poolParallelJobs: 50,

      // name of the pool
      // can be used to create different pools with elsewise identical options
      name: "my-pool"
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

可以看到,默认是启用cpus - 1worker 来实现多进程打包。

# 多进程并行压缩代码

上面我们提到了多进程打包,接下来应用一下可以并行压缩JavaScript 代码的插件。

webpack默认提供了UglifyJS插件来压缩JS代码,但是它使用的是单线程压缩代码,也就是说多个js文件需要被压缩,它需要一个个文件进行压缩。所以说在正式环境打包压缩代码速度非常慢(因为压缩JS代码需要先把代码解析成用Object抽象表示的AST语法树,再应用各种规则分析和处理AST,导致这个过程耗时非常大)。

这里介绍一款可以并行压缩代码的插件:terser-webpack-plugin

首先安装插件:

npm install terser-webpack-plugin --save-dev
1

然后在vue.config.js 中添加配置,注意该配置位于configureWebpack 下,并且建议在生产环境开启。

optimization: {
    minimize: true,
    minimizer: [
        new TerserPlugin({
            parallel: true,
            terserOptions: {
                output: {
                    comments: false, // remove comments
                },
                compress: {
                    warnings: false,
                    drop_console: true,
                    drop_debugger: true,
                    pure_funcs: ['console.log'], // remove console.log
                },
            },
            extractComments: false,
        }),
    ],
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

按照上述配置后,即可开启并行压缩 js 代码的功能。需要注意的是,V8 在系统上有内存的限制,默认情况下,32 位系统限制为 512M,64 位系统限制为 1024M。因为如果不加以限制,大型项目构建的时候可能会出现内存溢出的情况。也就是说,可能无法开启并行压缩的功能。但是压缩代码的功能还是可以正常使用的。

# 利用缓存提升二次构建速度

这里讨论下在webpack中如何利用缓存来提升二次构建速度。

webpack中利用缓存一般有以下几种思路:

  • babel-loader开启缓存
  • 使用cache-loader
  • 使用hard-source-webpack-plugin

这里重点介绍下第三种。HardSourceWebpackPlugin  为模块提供了中间缓存,缓存默认的存放路是: node_modules/.cache/hard-source

配置  hard-source-webpack-plugin后,首次构建时间并不会有太大的变化,但是从第二次开始,构建时间大约可以减少  80%左右。

首先安装插件:

npm install --save-dev hard-source-webpack-plugin
1

然后在vue.config.js 中添加配置,建议配置在开发环境,生产环境可能没有效果。

module.exports = {
  plugins: [new HardSourceWebpackPlugin()],
};
1
2
3

在第二次执行npm run serve 后,可以看到终端会有以下字样:

[hardsource:5e5f2c56] Using 144 MB of disk space.
[hardsource:5e5f2c56] Tracking node dependencies with: package-lock.json.
[hardsource:5e5f2c56] Reading from cache 5e5f2c56...
1
2
3

也就意味着构建从硬盘中读取缓存,加快了构建速度。

# 缩小构建目标

主要是exclude  与  include的使用:

  • exclude: 不需要被解析的模块
  • include: 需要被解析的模块

用的比较多的是排除/node_modules/ 模块。需要注意的是,exclude 权重更高,exclude 会覆盖 include 里的配置。

# 减少文件搜索范围

这个主要是resolve相关的配置,用来设置模块如何被解析。通过resolve的配置,可以帮助Webpack快速查找依赖,也可以替换对应的依赖。

  • resolve.modules:告诉  webpack  解析模块时应该搜索的目录
  • resolve.mainFields:当从  npm  包中导入模块时(例如,import * as React from 'react'),此选项将决定在  package.json  中使用哪个字段导入模块。根据  webpack  配置中指定的  target  不同,默认值也会有所不同
  • resolve.mainFiles:解析目录时要使用的文件名,默认是index
  • resolve.extensions:文件扩展名
resolve: {
    alias: {
      react: path.resolve(__dirname, './node_modules/react/umd/react.production.min.js')
    }, //直接指定react搜索模块,不设置默认会一层层的搜寻
    modules: [path.resolve(__dirname, 'node_modules')], //限定模块路径
    extensions: ['.js'], //限定文件扩展名
    mainFields: ['main'] //限定模块入口文件名
}
1
2
3
4
5
6
7
8

# 预编译资源模块

在使用webpack进行打包时候,对于依赖的第三方库,比如vuevuex等这些不会修改的依赖,我们可以让它和我们自己编写的代码分开打包,这样做的好处是每次更改本地代码的文件的时候,webpack只需要打包项目本身的文件代码,而不会再去编译第三方库。

那么第三方库在第一次打包的时候只打包一次,以后只要我们不升级第三方包的时候,那么webpack就不会对这些库去打包,这样的可以快速的提高打包的速度。其实也就是预编译资源模块

webpack中,我们可以结合DllPlugin  和  DllReferencePlugin插件来实现。

# DllPlugin 是什么

DLLPlugin  插件是在一个额外独立的webpack设置中创建一个只有dllbundle,也就是说我们在项目根目录下除了有vue.config.js,还会新建一个webpack.dll.config.js文件。

webpack.dll.config.js的作用是把所有的第三方库依赖打包到一个bundledll文件里面,还会生成一个名为  manifest.json文件。该manifest.json的作用是用来让  DllReferencePlugin  映射到相关的依赖上去的。

# DllReferencePlugin 又是什么

这个插件是在vue.config.js中使用的,该插件的作用是把刚刚在webpack.dll.config.js中打包生成的dll文件引用到需要的预编译的依赖上来。

什么意思呢?就是说在webpack.dll.config.js中打包后比如会生成  vendor.dll.js文件和vendor-manifest.json文件,vendor.dll.js文件包含了所有的第三方库文件,vendor-manifest.json文件会包含所有库代码的一个索引,当在使用vue.config.js文件打包DllReferencePlugin插件的时候,会使用该DllReferencePlugin插件读取vendor-manifest.json文件,看看是否有该第三方库。

vendor-manifest.json文件就是一个第三方库的映射而已。

话不多说,接下来看看怎么应用到项目中。

首先我们来编写webpack.dll.config.js 文件,内容如下:

const webpack = require("webpack");
const path = require("path");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");

module.exports = {
  mode: "production",
  entry: {
    vendor: [
      "vue/dist/vue.runtime.esm.js",
      "vuex",
      "vue-router",
      "vue-resource",
      "iview",
    ], // 这里是vue项目依赖的库
    util: ["lodash", "jquery", "moment"], // 这里是与框架无关的第三方库
  },
  output: {
    filename: "[name].dll.js",
    path: path.resolve(__dirname, "dll"),
    library: "dll_[name]",
  },
  plugins: [
    new CleanWebpackPlugin(), // clean-webpack-plugin目前已经更新到2.0.0,不需要传参数path
    new webpack.DllPlugin({
      name: "dll_[name]",
      path: path.join(__dirname, "dll", "[name].manifest.json"),
      context: __dirname,
    }),
  ],
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

在项目根目录上新建webpack.dll.config.js ,填写以上内容,同时还需要安装CleanWebpackPlugin ,步骤省略。

然后我们需要运行命令将第三方库打包到dll 文件夹下,该文件夹位于项目根目录。

执行命令如下:

webpack --config ./webpack.dll.config.js
1

执行上述命令时如果提示Do you want to install 'webpack-cli' (yes/no) ,输入yes 进行安装webpack-cli 。成功后会发现项目根目录生成dll 文件夹。文件夹下包含:

-util.dll.js - util.manifest.json - vendor.dll.js - vendor.manifest.json;
1

为了生成dll 文件夹方便,在package.json里面再添加一条脚本:

"scripts": {
    "build:dll": "webpack --config ./webpack.dll.config.js",
},
1
2
3

以后就可以执行npm run build:dll来生成 了。

接下来需要在vue.config.js 中添加以下代码:

const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin'); // 如果未安装请先安装


const dllReference = (config) => {
    config.plugin('vendorDll')
        .use(webpack.DllReferencePlugin, [{
            context: __dirname,
            manifest: require('./dll/vendor.manifest.json'),
        }]);


    config.plugin('utilDll')
        .use(webpack.DllReferencePlugin, [{
            context: __dirname,
            manifest: require('./dll/util.manifest.json'),
        }]);


    config.plugin('addAssetHtml')
        .use(AddAssetHtmlPlugin, [ // add-asset-html-webpack-plugin插件必须在html-webpack-plugin之后使用,因此这里要用webpack-chain来进行配置
            [
                {
                    filepath: require.resolve(path.resolve(__dirname, 'dll/vendor.dll.js')),
                    outputPath: 'dll',
                    publicPath: '/dll', // 这里的公共路径与项目配置有关,如果顶层publicPath下有值,请添加到dll前缀
                },
                {
                    filepath: require.resolve(path.resolve(__dirname, 'dll/util.dll.js')),
                    outputPath: 'dll',
                    publicPath: '/dll', // 这里的公共路径与项目配置有关,如果顶层publicPath下有值,请添加到dll前缀
                },
            ],
        ])
        .after('html'); // 'html'代表html-webpack-plugin,是因为@vue/cli-servide/lib/config/app.js里是用plugin('html')来映射的
};


module.exports = {
    publicPath: '/', // 顶层publiePath在这里
    chainWebpack: (config) => {
        if (process.env.NODE_ENV === 'production') { // 在开发环境中不使用dllPlugin是因为chrome的vue devtool是不能检测压缩后的vue源码,不方便代码调试
            dllReference(config);
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44