webpack分包vue

webpack SplitChunksPlugin vue-cl

2021-08-08  本文已影响0人  AizawaSayo

干货篇:
【webpack SplitChunksPlugin 配置详解】

【前端性能优化探讨及浏览器缓存机制】文末已经厘清,项目打包时要合理地合并/拆分 js,旨在控制单个资源体积的同时保证尽量少的请求次数( js 个数),避免请求高并发和资源过大导致阻塞加载。

然而光整js拆包还不够,最终输出的静态资源文件 (jscssimg 等),需采用内容摘要算法命名,以开启长期时效的强缓存。那就先以文件名配置作铺垫。

文件以内容摘要 hash 值命名以实现持久缓存

通过对output.filenameoutput.chunkFilename的配置,利用[contenthash]占位符,为js文件名加上根据其内容生成的唯一 hash 值,轻松实现资源的长效缓存。也就是说,无论是第几次打包,内容没有变化的资源 (如jscss) 文件名永远不会变,而那些有修改的文件就会生成新的文件名 (hash 值) 。

module.exports = {
  output: {
    path: __dirname + '/dist',
    filename: '[name].[contenthash:6].js',
    chunkFilename: '[name].[contenthash:8].js',
  },
}

如果是 webpack 4,还需要分别固定moduleIdchunkId,以保持名称的稳定性
因为 webpack 内部维护了一个自增的数字 id,每个 module 都有一个 id。当增加或删除 module 的时候,id 就会变化,导致其它 module 虽然内容没有变化,但由于 id 被强占,只能自增或者自减,导致整个项目的 module id 的顺序都错乱了。
也就是说,如果引入了一个新模块或删掉一个模块,都可能导致其它文件的 moduleId 发生改变,相应地文件内容也就改变,缓存便失效了
同样地,chunk 的新增/减少也会导致 chunk id 顺序发生错乱,那么原本的缓存就不作数了。

解决办法:

// node_modules/@vue/cli-service/lib/config/app.js
chainWebpack: config => {
  config
    .plugin('named-chunks')
      .use(require('webpack/lib/NamedChunksPlugin'), [chunk => {
        if (chunk.name) {
          return chunk.name
        }
        const hash = require('hash-sum')
        const joinedHash = hash(
          Array.from(chunk.modulesIterable, m => m.id).join('_')
        )
        return `chunk-` + joinedHash
      }])
}

在 webpack 5 optimization.chunkIds默认开发环境'named',生产环境'deterministic',因此我们无需设置该配置项。而且 webpack 5 更改了 id 生成算法,异步 chunk 也能轻松拥有固定的 id 了

至于图片和 CSS 文件

可以去看看 vue-cli 4 源码 @vue/cli-service/lib/config/下的配置处理,或者瞅【file-loader 配置详解以及资源相对路径处理】这篇,这里不详述。

SplitChunksPlugin 拆包实战

回归正题来讲代码分包。
SplitChunksPlugin 插件控制 webpack 打包输出的精髓就在于,提取公共代码,防止模块被重复打包、拆分过大的 js 文件、合并零散的 js 文件。但 js 体积和数量都要小这俩目标是相矛盾的,因此并没有标准的方案,需运用中庸之道,结合项目的实际情况去找到最合适的拆包策略。

vue-cli 4 默认处理

结合我用 vue-cli 4 搭的项目,来看下 vue-cli 通过 chainWebpack 覆盖掉 SplitChunksPlugin cacheGroups项默认值的配置(整理后):
(vue-cli chainWebpack配置处大致是node_modules/@vue/cli-service/lib/config/app.js:38)

module.exports = {
  entry: {
    app: './src/main',
  },
  output: {
    path: __dirname + '/dist',
    filename: 'static/js/[name].[contenthash:8].js',
    chunkFilename: 'static/js/[name].[contenthash:8].js',
  },
  optimization: {
    splitChunks: {
      chunks: 'async', // 只处理异步 chunk,这里两个缓存组都另配了 chunks,那么就被无视了 
      minSize: 30000, // 允许新拆出 chunk 的最小体积
      maxSize: 0, // 旨在与 HTTP/2 和长期缓存一起使用。它增加了请求数量以实现更好的缓存。它还可以用于减小文件大小,以加快二次构建速度。
      minChunks: 1, // 拆分前被 chunk 公用的最小次数
      maxAsyncRequests: 5, // 每个异步加载模块最多能被拆分的数量
      maxInitialRequests: 3, // 每个入口和它的同步依赖最多能被拆分的数量
      automaticNameDelimiter: '~',
      cacheGroups: { // 缓存组
        vendors: {
          name: `chunk-vendors`,
          test: /[\\/]node_modules[\\/]/,
          priority: -10, // 缓存组权重,数字越大优先级越高
          chunks: 'initial' // 只处理初始 chunk
        },
        common: {
          name: `chunk-common`,
          minChunks: 2, // common 组的模块必须至少被 2 个 chunk 共用 (本次分割前) 
          priority: -20,
          chunks: 'initial', // 只针对同步 chunk
          reuseExistingChunk: true  // 复用已被拆出的依赖模块,而不是继续包含在该组一起生成
        }
      },
    },
  },
};

我们配置了 webpack-bundle-analyzer 插件,便于观察和分析打包结果。

运行打包后,发现入口文件依赖的第三方包被全数拆出放进了chunk-vendors.js,剩下的同步依赖都被打包进了app.js,而其他都是懒加载组件生成的异步 chunk。并没有打包出所谓的公共模块合集chunk-common.js

入口依赖的第三方包 chunk

解读下此配置的拆分实现:

  1. 入口来自 node_modules 文件夹的同步依赖放入chunk-vendors
  2. 被至少 2 个 同步 chunk 共享的模块放入chunk-common
  3. 符合每个缓存组其他条件的情况下,能拆出的模块整合后的体积必须大于30kb(在进行 min+gz 之前的体积)。小了不生成新 chunk
  4. 每个异步引入模块并行请求的数量 (即它本身和它的同步依赖被拆分成的 js 个数)不能多于5个;每个入口文件和它的同步依赖最多能被拆成3个 js。
  5. 即使不匹配任何一个缓存组,splitChunks.* 级别的最小 chunk 属性minSize也会影响所有异步 chunk,效果是体积大于minSize值的公共模块会被拆出。(除非 splitChunks.* chunks: 'initial')
    公共模块即 >= 2个异步 chunk 共享的模块,同minChunks: 2
minSize 等属性参考标准

针对 3、4 两点作特别说明:vue-cli 4 内置 webpack 4,而 webpack 5 的 SplitChunksPlugin 的默认配置是不同的,如minSize: 20000, maxAsyncRequests: 30, maxInitialRequests: 30, enforceSizeThreshold: 50000。而maxSize默认值即为 0,不用像 webpack 4 这样额外设置。enforceSizeThreshold的用途是体积大于该值就对 chunk 进行强制拆分 (默认值约50kb)。
体积大于 maxSize 的 chunk 便能被拆分,为 0 表示不设限。因此只是作为一个提示存在,在 webpack 5 便被弱化了。同时需要满足的是 chunk 能拆出的模块不小于minSize值。
综上,webpack 5 能让 chunk 在合理的范围更细粒度地拆分,以便更好地支持和利用HTTP/2来进行长缓存。 故 3、4 两点我们会根据当下标准重新配置。
所以查 Api 的时候切记要弄清版本

同时我们发现,部分 node_modules 包被重复打包进了一些异步加载的 js 中 (如下)。

两个异步 chunk 的公共模块分出的包

这个 js 是根据上面第 5 点生成的,另如果对异步 chunk 名字有疑问,是我在动态引入的时候用了 webpackChunkName magic comment(魔术注释)。此处为两个异步 chunk 名用'~'分隔符连接是为了说明模块来源,也是 webpack 的自行处理。
【SplitChunksPlugin 干货篇】已经讲得很详尽,这里不再重复。

它其实是两个异步模块guide-addguide-edit共同引用的组件,由于体积过大 (超过minSize) 被 webpack 单独拆分出来。而且据观察其实大部分懒加载组件都未引入第三包,那这个code-js的重复就更显得突兀和没有必要了。
这和没有打包出任何公共模块(chunk-common) ,都是chunks: 'initial'的锅。这俩缓存组都只负责拆入口 (entry point) 和其同步依赖的模块,异步 chunk 里的第三方自然拆不出来。而且单入口的情况默认生成的 initial chunk 只有一个,上哪和其他同步 chunk 共享模块呀 (minChunks: 2的意思是至少 2 个 chunk 共同引入的同步模块) 。

必须清楚minChunks的共用是面向 chunk 的,有些文章会误写成模块之间共享。同时了解 SplitChunksPlugin 拆包前 webpack 对于 chunk 的初始分包状态也至关重要。不清楚可以 ➡️ 【webpack SplitChunksPlugin 配置详解】 开篇处)。

还有chunk-vendors.jsapp.js的体积都太大了,特别是初始第三方包竟有 841kb。非常不利于首屏加载的响应速度。以上说明 vue-cli 4 的处理还是有些不尽人意,那我们来自行优化看看吧。

拆包优化

再回顾下这张图:

再回观我们之前的app.jschunk-vendors.js。它们都是初始加载的 js,由于体积太大需要在合理范围内拆分成更小一些的 js,以利用浏览器的并发请求,优化首页加载体验。

splitChunks: {
  chunks: "all",
  minSize: 20000, // 允许新拆出 chunk 的最小体积,也是异步 chunk 公共模块的强制拆分体积
  maxAsyncRequests: 6, // 每个异步加载模块最多能被拆分的数量
  maxInitialRequests: 6, // 每个入口和它的同步依赖最多能被拆分的数量
  enforceSizeThreshold: 50000, // 强制执行拆分的体积阈值并忽略其他限制
  cacheGroups: {
    libs: { // 第三方库
      name: "chunk-libs",
      test: /[\\/]node_modules[\\/]/,
      priority: 10,
      chunks: "initial" // 只打包初始时依赖的第三方
    },
    elementUI: { // elementUI 单独拆包
      name: "chunk-elementUI",
      test: /[\\/]node_modules[\\/]element-ui[\\/]/,
      priority: 20 // 权重要大于 libs
    },
    svgIcon: { // svg 图标
      name: 'chunk-svgIcon',
      test(module) {
        // `module.resource` 是文件的绝对路径
        // 用`path.sep` 代替 / or \,以便跨平台兼容
        // const path = require('path') // path 一般会在配置文件引入,此处只是说明 path 的来源,实际并不用加上
        return (
          module.resource &&
          module.resource.endsWith('.svg') &&
          module.resource.includes(`${path.sep}icons${path.sep}`)
        )
      },
      priority: 30
    },
    commons: { // 公共模块包
      name: `chunk-commons`,
      minChunks: 2, 
      priority: 0,
      reuseExistingChunk: true
    }
  },
};
最终打包结果 现在的`app.js` 异步 chunk 中拆出的公共模块

格式美化后的index.html引入的 js 如下:

index.html script 脚本部分

当然还可以更细化地拆分,比如拆出全局组件、第三方里再拆出个较大的包/或者直接用 CDN 引入。其实优化就是一个博弈的过程,抉择让 a bundle 大一点还是 b bundle? 是让首次加载快一点还是让 cache 的利用率高一点?不要过度追求颗粒化的前提下,尽量利用浏览器缓存就可以啦。

上一篇下一篇

猜你喜欢

热点阅读