close
CC 4.0 协议

本节内容派生于以下链接指向的内容 ,并遵守 CC BY 4.0 许可证的规定。

以下内容如果没有特殊声明,可以认为都是基于原内容的修改和删减后的结果。

SplitChunksPlugin

SplitChunksPlugin 是一个内置插件,用于将代码拆分成多个 chunk,以优化应用的加载性能,实现更好的缓存策略和并行加载效果。

SplitChunksPlugin 可以通过 optimization.splitChunks 选项进行配置,通常你不需要手动注册该插件。

默认行为

Rspack 内置了开箱即用的 SplitChunksPlugin 配置,适用于大部分场景。

默认情况下,它只会影响到按需加载的 chunk,因为初始 chunk 会影响到项目的 HTML 文件中的脚本标签。

Rspack 默认提供了两个 cacheGroup:

  • default:抽离至少被 2 个 chunk 共享的模块。
  • defaultVendors:抽离来自 node_modules 的模块。

Rspack 将根据以下条件自动拆分 chunk:

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

默认情况下,chunks 的值为 async,但对大多数生产环境应用,建议优先使用:

rspack.config.mjs
export default {
  optimization: {
    splitChunks: {
      chunks: 'all',
    },
  },
};

同时保留默认的 cacheGroups 配置。这样通常可以同时对 initial chunk 和 async chunk 去重,并且不会改变 JavaScript 的执行语义。Rspack runtime 仍然只会加载当前入口或 runtime 实际可达的 chunk。

配置

Rspack 为希望对该功能进行更多控制的开发者提供了一组选项。

Warning

Rspack 的默认配置符合 Web 性能最佳实践,但是项目的最佳策略可能有所不同。如果要更改配置,则应评估所做更改的影响,以确保有真正的收益。

optimization.splitChunks

下面这个配置对象代表 SplitChunksPlugin 的默认行为。

rspack.config.mjs
export default {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async',
      minChunks: 1,
      minSize: 20000,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      cacheGroups: {
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          reuseExistingChunk: true,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};
Warning

当 Rspack 处理文件路径时,它们始终包含 UNIX 系统中的 / 和 Windows 系统中的 \。这就是为什么在 {cacheGroup}.test 字段中使用 [\\/] 来表示路径分隔符的原因。{cacheGroup}.test 中的 /\ 会在跨平台使用时产生问题。

Warning

Rspack 不允许将 entry 名称传递给 {cacheGroup}.test 或者为 {cacheGroup}.name 使用现有的 chunk 的名称。

splitChunks.cacheGroups

cacheGroup 可以继承和/或覆盖来自 splitChunks.{cacheGroup}.* 的任何选项。但是 testpriorityreuseExistingChunk 只能在 cacheGroup 级别上进行配置。将它们设置为 false 以禁用任何默认的 cacheGroup。

rspack.config.mjs
export default {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        default: false,
      },
    },
  },
};

splitChunks.chunks

splitChunks.cacheGroups.{cacheGroup}.chunks

  • 类型:
type OptimizationSplitChunksChunks =
  | 'initial'
  | 'async'
  | 'all'
  | RegExp
  | ((chunk: Chunk) => boolean);
  • 默认值: 'async'

此选项用于控制哪些 chunk 参与代码拆分。当传入字符串时,可选值包括 allasyncinitial

  • all:拆分所有类型的 chunks,包括 initial chunksasync chunks
  • initial:仅拆分 initial chunks。
  • async:仅拆分 async chunks。

通常来说,设置为 all 会有助于减少被重复打包的模块,因为这意味着 chunks 可以被 initial chunks 和 async chunks 共享。

对于大多数生产环境应用,chunks: 'all' 是推荐的起点。真正容易把结果搞差的通常不是 chunks: 'all' 本身,而是过于宽泛的手写 cacheGroup 再配合固定的 name

rspack.config.mjs
export default {
  optimization: {
    splitChunks: {
      // include all types of chunks
      chunks: 'all',
    },
  },
};
Tip

在 Rspack v1.6.2 之前,使用 模块联邦 并配置 exposes 时无法开启 chunks: 'all',否则会破坏远程模块拆分。

从 v1.6.2 起,可以同时使用模块联邦和 chunks: 'all'

chunks 选项可以设置为正则表达式,它是 (chunk) => typeof chunk.name === "string" && regex.test(chunk.name) 的缩写。

rspack.config.mjs
export default {
  optimization: {
    splitChunks: {
      // 等价于 `chunks: (chunk) => typeof chunk.name === "string" && /foo/.test(chunk.name)`
      chunks: /foo/,
    },
  },
};

chunks 选项可以设置为函数来进行更细粒度的控制。该函数接收一个 chunk 参数,返回值为 true 表示该 chunk 参与拆分(其中的模块可能被提取到新的 chunk 中),返回值为 false 表示该 chunk 不参与拆分(保持原样)。

rspack.config.mjs
export default {
  optimization: {
    splitChunks: {
      chunks(chunk) {
        // exclude `foo` chunk
        return chunk.name !== 'foo';
      },
    },
  },
};
Warning

使用函数形式的 chunks 会显著降低构建性能,因为该函数需要对每个模块进行调用,从而产生大量的 Rust 与 JavaScript 之间的跨语言通信开销。因此我们不推荐使用函数形式。

你可以为每个 cacheGroup 单独配置 chunks,例如:

rspack.config.mjs
export default {
  optimization: {
    splitChunks: {
      cacheGroups: {
        groupA: {
          chunks: 'all',
        },
        groupB: {
          chunks: 'initial',
        },
        groupC: {
          chunks: 'async',
        },
      },
    },
  },
};

splitChunks.maxAsyncRequests

  • 类型: number
  • 默认值: 30

按需加载时的最大并行请求数。

splitChunks.maxInitialRequests

  • 类型: number
  • 默认值: 30

每一个入口点所允许的的最大并行请求数。

splitChunks.minSize

splitChunks.cacheGroups.{cacheGroup}.minSize

  • 类型: number | Record<string, number>
  • 默认值: 生产环境下为 20000, 其余为 10000

生成 chunk 的最小体积(以 bytes 为单位)。

当使用 number 类型的配置时,会为所有的模块类型配置同样的 minSize,模块类型为 splitChunks.defaultSizeTypes 中定义的模块类型。

rspack.config.mjs
export default {
  //...
  optimization: {
    splitChunks: {
      minSize: 100 * 1000,
    },
  },
};

当使用对象形式配置时,可以为不同类型的模块类型分别配置不同的 minSize

rspack.config.mjs
export default {
  //...
  optimization: {
    splitChunks: {
      minSize: {
        javascript: 100 * 1000,
        css: 300 * 1000,
      },
    },
  },
};

例如上面的配置表示拆分出的 chunk 中 javascript 模块的最小体积需要满足 100KB,css 模块的最小体积需要满足 300KB。

splitChunks.minSizeReduction

splitChunks.cacheGroups.{cacheGroup}.minSizeReduction

  • 类型: number | Record<string, number>
  • 默认值: 0

当构建产物中存在多个小型模块时,即使这些模块的总体积超过 minSize 设定的值,开发者可能也不希望为它们生成独立的 chunk。此时,可以通过 minSizeReduction 参数设定模块拆分时必须达到的最小体积缩减阈值。

该参数的计算规则是:只有当模块被拆分后,所有父 chunk 的减少的总体积不低于设定值时,才会进行拆分。

假设存在以下场景,某个模块的体积为 40KB,它被 2 个 chunk 同时引用,我们配置了 minSizeReduction: 100。如果将该模块拆分出去,每个父 chunk 可以减少 40KB 体积,总共可以减少 40KB × 2 = 80KB 的体积, 不足 100KB,因此不触发拆分。

如果你已经调低了 minSize,模块仍然重复出现,下一步应优先检查 minSizeReduction

rspack.config.mjs
export default {
  //...
  optimization: {
    splitChunks: {
      minSizeReduction: 100 * 1000,
    },
  },
};

splitChunks.minChunks

splitChunks.cacheGroups.{cacheGroup}.minChunks

  • 类型: number
  • 默认值: 1

拆分前必须共享模块的最小 chunk 数。

splitChunks.hidePathInfo

  • 类型: boolean
  • 默认值: options.mode'production' 时默认为 true

是否隐藏路径名。

splitChunks.maxSize

使用 maxSize(全局配置 optimization.splitChunks.maxSize、每个 cacheGroup 的 optimization.splitChunks.cacheGroups[x].maxSize,或 fallback cacheGroup 的 optimization.splitChunks.fallbackCacheGroup.maxSize)告诉 Rspack 尝试将大于 maxSize 个字节的 chunk 分割成较小的部分。 这些较小的部分在体积上至少为 minSize(仅次于 maxSize)。 该算法是确定性的,对模块的更改只会产生局部影响。这样,在使用长期缓存时就可以使用它并且不需要记录。maxSize 只是一个提示,当模块大于 maxSize 或者拆分不符合 minSize 时可能会被违反。

Rspack 在处理 maxSize 时会基于路径派生出的确定性 key 对模块进行分组,因此路径相近的模块往往会尽量留在一起。实践中,如果问题是“某个共享 chunk 太大了”,通常优先考虑 maxSize,而不是使用宽泛的固定 name

当 chunk 已经有一个名称时,每个部分将获得一个从该名称派生的新名称。 根据 optimization.splitChunks.hidePathInfo 的值,它将添加一个从第一个模块名称或其哈希值派生的密钥。

maxSize 选项旨在与 HTTP/2 和长期缓存一起使用。它增加了请求数量以实现更好的缓存。它还可以用于减小文件大小,以加快二次构建速度。

Tip

maxSizemaxInitialRequest/maxAsyncRequests 具有更高的优先级。实际优先级是 maxInitialRequest/maxAsyncRequests < maxSize < minSize

Tip

设置 maxSize 的值会同时设置 maxAsyncSizemaxInitialSize 的值。

splitChunks.maxAsyncSize

  • 类型: number | Record<string, number>

maxSize 一样,maxAsyncSize 可以全局应用(splitChunks.maxAsyncSize),也可以应用到 cacheGroup(splitChunks.cacheGroups.{cacheGroup}.maxAsyncSize)或 fallback cacheGroup(splitChunks.fallbackCacheGroup.maxAsyncSize)。

maxAsyncSizemaxSize 的区别在于 maxAsyncSize 仅会影响按需加载 chunk。

splitChunks.maxInitialSize

  • 类型: number | Record<string, number>

maxSize 一样,maxInitialSize 可以全局应用(splitChunks.maxInitialSize),也可以应用到 cacheGroup(splitChunks.cacheGroups.{cacheGroup}.maxInitialSize)或 fallback cacheGroup(splitChunks.fallbackCacheGroup.maxInitialSize)。

maxInitialSizemaxSize 的区别在于 maxInitialSize 仅会影响初始加载 chunks。

splitChunks.automaticNameDelimiter

  • 类型: string
  • 默认值: -

默认情况下,Rspack 将使用 chunk 的来源和名称生成名称(例如 vendors-main.js)。此选项使你可以指定用于生成名称的分隔符。

splitChunks.name

splitChunks.cacheGroups.{cacheGroup}.name

  • 类型: string | function
  • 默认值: false

其中函数类型的版本要求为 >=0.4.1

每个 cacheGroup 也可以使用:splitChunks.cacheGroups.{cacheGroup}.name

拆分 chunk 的名称。设为 false 将保持 chunk 的相同名称,因此不会不必要地更改名称。这是生产环境下构建的建议值。

name 不只是文件名定制选项,它还会改变分组行为:当不同的 chunk 组合命中了同一个 cacheGroup,并且最终解析到相同的 name 时,Rspack 会把它们合并成同一个命名后的拆分候选。这样虽然可能提高缓存命中率,但也可能让某些页面去请求本来不需要的模块依赖链。

如果你的目标只是给 chunk 一个更稳定的标识或文件名提示,优先考虑使用 splitChunks.cacheGroups.{cacheGroup}.idHint,并保持 name 未设置。

如果 splitChunks.name 与 entry point 名称匹配,entry point 将被删除。

Info

splitChunks.cacheGroups.{cacheGroup}.name 可以用来将模块移到其所属 Chunk 的的父 Chunk 中。例如,使用 name: "entry-name " 来将模块移到 entry-name chunk 中。你也可以使用按需命名的 chunk,但你必须注意所选的模块只在这个 chunk 下使用。

splitChunks.filename

splitChunks.cacheGroups.{cacheGroup}.filename

  • 类型: string | function

仅在初始 chunk 时才允许覆盖文件名。 也可以在 output.filename 中使用所有占位符。

rspack.config.mjs
export default {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        defaultVendors: {
          filename: 'vendors-[name].js',
          // or
          filename: (pathData, assetInfo) => {
            return `${pathData.chunk.name}-bundle.js`;
          },
        },
      },
    },
  },
};

splitChunks.usedExports

开启该配置后,在拆分 chunk 将根据 module 在不同 runtime 中导出的使用情况进行分组,保持在不同 runtime 中都是最优的加载体积。

举个例子,如果一次构建中有 3 个入口分别名为 foo, bar 和 baz,他们都依赖了一个相同的模块 shared,但 foo 和 bar 依赖 shared 中的导出 value1,而 baz 依赖了 shared 中的导出 value2。

foo.js
import { value1 } from 'shared';
value1;
bar.js
import { value1 } from 'shared';
value1;
baz.js
import { value2 } from 'shared';
value2;

默认的策略中 shared 模块由于同时出现在 3 个 chunk 中,如果它满足了最小拆分体积,那么 shared 本该被抽离到一个单独 chunk 中。

chunk foo, chunk bar
      \
      chunk shared (exports value1 and value2)
      /
chunk baz

但这样会导致 3 个入口都不满足最佳的加载体积,从 foo 和 bar 入口加载 shared 会多加载并不需要的导出 value2,而从 baz 入口加载 shared 会多加载并不需要的导出 value1。

当开启 splitChunks.usedExports 优化后,会分析 shared 模块分别在不同入口中用到的导出,发现在 foo 和 bar 中用到的导出和 baz 中不一样,会产生 2 个不同的 chunk,其中一个对应入口 foo 和 bar,另一个对应入口 baz。

chunk foo, chunk bar
        \
      chunk shared-1 (exports only value1)

chunk baz
        \
      chunk shared-2 (exports only value2)

这个选项只会改变 splitChunks 在不同 runtime 之间的分组方式,并不等价于 tree shaking,也不会决定未使用导出是否被删除。

splitChunks.defaultSizeTypes

  • 类型: string[]
  • 默认值: ["javascript", "css", "unknown"]

在计算 chunk 大小时的模块类型,默认只有 javascript 模块和内置的 css 模块的体积会被计算在内,例如配置 minSize: 300 时,javascript 模块和 css 模块的体积都需要满足要求才能拆分。

你可以配置额外的模块类型,例如想让 WebAssembly 模块也进行拆分:

rspack.config.mjs
export default {
  optimization: {
    splitChunks: {
      defaultSizeTypes: ['wasm', '...'],
    },
  },
};

splitChunks.cacheGroups

cacheGroup 可以继承和/或覆盖来自 splitChunks.* 的任何选项。但是 testpriorityreuseExistingChunk 只能在 cacheGroup 级别上进行配置。将它们设置为 false 以禁用任何默认的 cacheGroup。

rspack.config.mjs
export default {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        default: false,
      },
    },
  },
};

splitChunks.cacheGroups.{cacheGroup}.priority

  • 类型: number
  • 默认值: -20

一个模块可以属于多个 cacheGroup。优化会优先选择 priority(优先级)更高的 cacheGroup。默认组的优先级为负,以便自定义组获得更高的优先级(自定义组的默认值为 0)。

splitChunks.cacheGroups.{cacheGroup}.test

  • 类型: RegExp | string | (module: Module, { chunkGraph: ChunkGraph, moduleGraph: ModuleGraph }) => boolean

其中函数类型的版本要求为 >=0.4.1

控制此 cacheGroup 选择的模块。省略它会选择所有模块。它可以匹配绝对模块资源路径或 chunk 名称。匹配 chunk 名称时,将选择该 chunk 中的所有模块。

Warning

使用函数形式的 test 会显著降低构建性能,因为该函数需要对每个模块进行调用,从而产生大量的 Rust 与 JavaScript 之间的跨语言通信开销。因此我们不推荐使用函数形式。

splitChunks.cacheGroups.{cacheGroup}.enforce

  • 类型: boolean

告诉 Rspack 忽略 splitChunks.minSizesplitChunks.minChunkssplitChunks.maxAsyncRequestssplitChunks.maxInitialRequests 选项,并始终为此 cacheGroup 创建 chunk。

splitChunks.cacheGroups.{cacheGroup}.idHint

  • 类型: string

设置 chunk id 的提示。 它将被添加到 chunk 的文件名中。

splitChunks.cacheGroups.{cacheGroup}.reuseExistingChunk

  • 类型: boolean
  • 默认值: false

是否重用现有的 chunk。如果经过拆分后新产生的 chunk 中包含的模块和原 chunk 中包含的模块完全一致,则原 chunk 会被复用,不会生成新的 chunk,这可能会影响 chunk 最终的文件名。举例:

chunk Foo: [ module A, module B ]
chunk Bar: [ module B ]

cacheGroup: {
  test: /B/,
  chunks: 'all'
}

由于 cacheGroup 的配置,在 chunk Foo 和 chunk Bar 中的 module B 会被拆分到一个新的 chunk 中,该 chunk 只包含 module B,这个新 chunk 和 chunk Bar 包含的 module 完全一样,因此可以直接复用 chunk Bar。

如果设置 reuseExistingChunk 为 false,那么 chunk Bar 和 chunk Foo 中的 module B 会被移到新 chunk 中,chunk Bar 由于不包含任何 module,会作为空 chunk 被删除。

splitChunks.cacheGroups.{cacheGroup}.type

  • Types: string | RegExp

允许模块根据模块类型分配给 cache group

常见问题

为什么模块仍然会重复?

常见原因包括:

很小的重复模块往往是有意为之。把它们额外抽成一个 chunk,未必比保留重复更好。

splitChunks 会影响 JavaScript 执行顺序吗?

不会。splitChunks 只会改变 chunk 的拓扑结构,JavaScript 的加载和执行顺序仍然由 runtime 依赖图保证。

splitChunks 会影响 tree shaking 吗?

不会。tree shaking 决定哪些代码会被保留;splitChunks 只决定已经保留的模块如何被分配到不同 chunk 中。

splitChunks 会影响 CSS 顺序吗?

有可能。

mini-css-extract-plugin、Rspack 的内置 CSS 支持这类抽取式 CSS 场景里,splitChunks 改变 chunk group 后,最终 CSS 顺序也可能随之变化。可以参考 Deep dive into webpack CSS order issue。如果 CSS 顺序是主要诉求,也建议同时参考 CssChunkingPlugin