从0到1:webpack5 搭建 Vue3 项目——打包优化

1,013 阅读6分钟

前两篇文章介绍了如何使用 webpack5 搭建一个 Vue3 的 开发环境生产环境,这篇文章继续介绍在打包时如何做一些优化。

可以从 GitHub 获取本文所对应的代码

打包分析工具 webpack-bundle-analyzer

要做打包优化,首先我们得知道自己打出来的包的情况,我们使用 webpack-bundle-analyzer 来做打包分析工具。

安装:

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

webpack.config.prod.js 中进行配置:

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const prodWebpackConfig = merge(baseWebpackConfig, {
  plugins: [
    // 打包分析插件
    new BundleAnalyzerPlugin()
  ]
});

重新执行 npm run build 命令,在打包结束之后,会自动打开浏览器,加载http://127.0.0.1:8888 地址,显示当前的打包情况。

image.png

这里显示的是打包出来的 js 文件的情况。

打包优化时,我们的目的是控制 js 文件的大小和数量,主要从以下几个方面来优化。

使用 SplitChunksPlugin

webpack4 之后,内置了 SplitChunksPlugin 用来提取公用 chunk,我们直接配置即可。

默认情况下,它只会影响到按需加载的 chunks。

webpack 将根据以下条件自动拆分 chunks:

  • 新的 chunk 可以被共享,或者模块来自于 node_modules 文件夹
  • 新的 chunk 体积大于 20kb(在进行 min+gz 之前的体积)
  • 当按需加载 chunks 时,并行请求的最大数量小于或等于 30
  • 当加载初始化页面时,并发请求的最大数量小于或等于 30

当尝试满足最后两个条件时,最好使用较大的 chunks。

在 webpack 中的默认配置如下:

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async',
      minSize: 20000,
      minRemainingSize: 0,
      minChunks: 1,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      enforceSizeThreshold: 50000,
      cacheGroups: {
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          reuseExistingChunk: true,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

提取公共 chunk

chunks 默认为 async,只对异步导入的模块进行提取公共 chunk,而同步引入的,不会提取,像我们在项目中使用的 vuevue-router,它在打包时,直接打入了main 里面。

chunks还有两个可选值:

  • initial - 同步引入的库进行分离;
  • all - 所有引入的库进行分离;

在打包时,推荐使用 all

项目开发中,我们对有些模块,比如 Util.js 文件,会在多个文件里面引入并使用,如果不做提取,会在每个文件打包时,都将 Util.js 的内容复制一份,最终打出来的包体积会较大,而这些重复的代码又是没必要的。

我们可以通过配置 cacheGroups 来控制公共 chunk 的提取。

在 webpack 的默认配置中,已经有了 defaultVendorscacheGroups.default 两个配置。
defaultVendors 里面配置了 test 字段,后面是一个正则表达式,表示将那些从 node_modules 引入的模块,提取到一个公共 chunk 中;
default 里面没有配置 test,表示这个规则会应用到所有模块上,minChunks: 2 表示当某个文件至少被2个 chuns 使用时,将其提取到公共 chunk。

当然,我们也可以根据自己的需要,继续在 cacheGroups 中添加配置,将一些模块打包进一个单独的 chunk,比如下面的配置,会将 vue-routervuex 打包进一个名为 vue_lib 的 chunk 中:

  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vue_lib: {
          test: /[\\/]node_modules[\\/](vue-router|vuex)/,
          name: 'vue_lib'
        },
      }
    }
  }

在配置 splitChunks 时,需要注意以下几个配置字段:

  • minSize,生成 chunk 的最小体积(以 bytes 为单位),即使我们在 cacheGroups 里添加了新的 chunk 配置,但是如果这些模块抽取出来生成的新 chunk 的大小,还未达到 miniSize 设置的值,是不会生成新 chunk 的;

  • maxSize,告诉 webpack ,尝试将大于 maxSize 个字节的 chunk 分割成较小的部分。这些较小的部分在体积上至少为 minSize(仅次于 maxSize)。 maxSize 只是一个提示,当模块大于 maxSize 或者拆分不符合 minSize 时可能会被违反,换句话说,我们设置了 maxSize 并不意味着当 chunk 大于这个值时,就一定会被拆分成多个 chunk,因为还有很多其它的条件在。

  • cacheGroups 中配置 chunk 时,可以在里面覆盖 splitChunks 里面的配置。比如我们可以 vue_lib 的配置里面,重新设置 minSize

    optimization: {
        splitChunks: {
          chunks: 'all',
          minSize: 20000,
          cacheGroups: {
            vue_lib: {
              test: /[\\/]node_modules[\\/](vue-router|vuex)/,
              name: 'vue_lib',
              minSize: 10000, // 覆盖 splitChunks.minSize 配置
            },
          }
        }
      }
    
  • minChunks,只有当某个模块被其它 chunk 使用的次数大于等于这个数值时,才会被抽取到新 chunk 中。

以上这些都是从 webpack 的配置入手,对已有的代码进行分包,它的能力也是有限的。
我们在写代码的时候,就要注意对模块的引用方式,对于那些不需要在一开始就加载的模块,使用异步引入的方式。打包的时候,这些异步引入的模块也会被打进单独的 chunk 中,在浏览器加载时,让这些模块按需加载。

gzip 压缩

文件大小是直接影响页面打开速度的因素,默认情况下,webpack 对打包出来的文件是没有压缩的。我们通过 webpack-bundle-analyzer 查看打包文件情况时,左侧的选项中有一个 Gzipped,选中之后,显示的文件大小,就是被压缩之后的文件大小。

image.png

Stat - 表示原始文件大小;
Parsed - 表示 webpack 打包之后的文件大小;
Gzipped - 表示压缩后的文件大小。
文档链接

可以看到,压缩前和压缩后,文件大小的差距还是很大的。

使用 compression-webpack-plugin

安装 compression-webpack-plugin 插件:

npm install compression-webpack-plugin --save-dev

webpack.config.prod.js 中配置:

const CompressionPlugin = require("compression-webpack-plugin");

module.exports = {
  plugins: [
    new CompressionPlugin({
      test: /.(js|css)(\?.*)?$/, // 只压缩 js 和 css 文件
      algorithm: 'gzip', // 压缩算法,默认为 gzip
      threshold: 10240, // 文件大于 10 KB 时才压缩,默认为 0
      minRatio: 0.8, // 只有当文件压缩后的比率小于这个值时,文件才会被压缩,默认值 0.8
      deleteOriginalAssets: false, // 是否删除原文件,默认为 false
    }),
  ],
};

重新打包,在 dist/jsdist/css 文件夹下面,都会生成以 .gz 为后缀名的文件,这些就是压缩过的文件。

到这里只是完成了第一步,当项目部署到服务器之后,需要后端开启 gzip,否则还是返回的未压缩的文件。当后端开启 gzip 后,在请求 js 文件时,响应头中会有 Content-Encoding: gzip

我们可以在本地起一个服务来测试,这里需要安装一下 http-server

dist 文件夹下,直接执行 http-server 命令启动服务,默认情况下是没有开启 gzip 的,

image.png

我们访问 http://localhost:8080,在调试工具的 network 中查看文件请求:

image.png

然后重新使用 http-server -g 命令启动服务,开启 gzip:

image.png

在浏览器中重新加载:

image.png

此时加载的就是压缩过的文件,在响应头中可以看到 Content-Encoding 字段,且值为 gzip

image.png

前两篇文章:
从0到1:webpack5 搭建 Vue3 项目——开发环境
从0到1:webpack5 搭建 Vue3 项目——生产环境