Skip to content

自动代码分割

自动代码分割是从模块创建 chunk 的过程。本章描述它的行为及其背后的原理。

自动代码分割不可手动控制。它会按照某些规则运行。因此,我们也将其与手动代码分割区分开来,前者是自动代码分割,后者是手动代码分割。

自动代码分割会生成两种类型的 chunk。

入口 chunk

入口 chunk 是通过将静态连接的模块合并到一个 chunk 中而生成的。"静态"指的是静态 import ... from '...'require(...)

入口 chunk 有两种类型。

第一种是初始 chunk初始 chunk 是由于用户配置而生成的。例如,input: ['./a.js', './b.js'] 定义了两个初始 chunk

第二种是动态 chunk动态 chunk 是由于动态导入而生成的。动态导入用于按需加载代码,因此我们不会把被导入的代码与导入者放在一起。

对于以下代码,将生成两个 chunk:

js
// entry.js(包含在 `input` 选项中)
import foo from './foo.js';
import('./dyn-entry.js');

// dyn-entry.js
require('./bar.js');

// foo.js
export default 'foo';

// bar.js
module.exports = 'bar';

在这种情况下,存在两组静态连接的模块。

cluster_group1组 1(初始 chunk)cluster_group2组 2(动态 chunk)entryentry.jsfoofoo.jsentry->foo静态导入dyndyn-entry.jsentry->dynimport()barbar.jsdyn->barrequire()
cluster_group1组 1(初始 chunk)cluster_group2组 2(动态 chunk)entryentry.jsfoofoo.jsentry->foo静态导入dyndyn-entry.jsentry->dynimport()barbar.jsdyn->barrequire()

由于存在两组,最终自动代码分割会生成两个 chunk。

公共 chunk

当某个模块被至少两个不同的入口静态导入时,就会生成公共 chunk。这些模块会被放入一个单独的 chunk 中。

这种行为的目的是:

  • 确保最终 bundle 输出中的每个 JavaScript 模块都是单例。
  • 当一个入口执行时,只应执行被导入的模块。

需要注意的是,某个模块是否可以被放入同一个公共 chunk,取决于它是否被相同的入口导入。

对于以下代码,将生成六个 chunk:

js
// entry-a.js (包含在 `input` 选项中)
import 'shared-by-ab.js';
import 'shared-by-abc.js';
console.log(globalThis.value);

// entry-b.js (包含在 `input` 选项中)
import 'shared-by-ab.js';
import 'shared-by-bc.js';
import 'shared-by-abc.js';
console.log(globalThis.value);

// entry-c.js (包含在 `input` 选项中)
import 'shared-by-bc.js';
import 'shared-by-abc.js';
console.log(globalThis.value);

// shared-by-ab.js
globalThis.value = globalThis.value || [];
globalThis.value.push('ab');

// shared-by-bc.js
globalThis.value = globalThis.value || [];
globalThis.value.push('bc');

// shared-by-abc.js
globalThis.value = globalThis.value || [];
globalThis.value.push('abc');

这些 chunk 将按如下方式生成:

js
import './common-ab.js';
import './common-abc.js';
js
import './common-ab.js';
import './common-bc.js';
import './common-abc.js';
js
import './common-bc.js';
import './common-abc.js';
js
globalThis.value = globalThis.value || [];
globalThis.value.push('ab');
js
globalThis.value = globalThis.value || [];
globalThis.value.push('bc');
js
globalThis.value = globalThis.value || [];
globalThis.value.push('abc');

下面的图展示了入口如何共享依赖,以及模块如何被分组到 chunk 中:

cluster_chunk_aentry-a.js chunkcluster_chunk_bentry-b.js chunkcluster_chunk_centry-c.js chunkcluster_common_abcommon-ab.js chunkcluster_common_bccommon-bc.js chunkcluster_common_abccommon-abc.js chunkentry_aentry-a.jsshared_abshared-by-ab.jsentry_a->shared_abshared_abcshared-by-abc.jsentry_a->shared_abcentry_bentry-b.jsentry_b->shared_abshared_bcshared-by-bc.jsentry_b->shared_bcentry_b->shared_abcentry_centry-c.jsentry_c->shared_bcentry_c->shared_abc
cluster_chunk_aentry-a.js chunkcluster_chunk_bentry-b.js chunkcluster_chunk_centry-c.js chunkcluster_common_abcommon-ab.js chunkcluster_common_bccommon-bc.js chunkcluster_common_abccommon-abc.js chunkentry_aentry-a.jsshared_abshared-by-ab.jsentry_a->shared_abshared_abcshared-by-abc.jsentry_a->shared_abcentry_bentry-b.jsentry_b->shared_abshared_bcshared-by-bc.jsentry_b->shared_bcentry_b->shared_abcentry_centry-c.jsentry_c->shared_bcentry_c->shared_abc

entry-*.js chunks 是根据上面讨论的原因生成的。common-*.js chunks 是公共 chunk。它们之所以被创建,是因为:

  • common-ab.jsshared-by-ab.jsentry-a.jsentry-b.js 同时导入。
  • common-bc.jsshared-by-bc.jsentry-b.jsentry-c.js 同时导入。
  • common-abc.jsshared-by-abc.js 被全部 3 个入口导入。

你可能会问,为什么自动代码分割不把 shared-by-*.js 文件放入单个公共 chunk。原因是这样做会违背原始代码的意图。

对于上面的示例,如果创建一个单独的公共 chunk,它将类似于:

common-all.js
js
globalThis.value = globalThis.value || [];
globalThis.value.push('ab');
globalThis.value = globalThis.value || [];
globalThis.value.push('bc');
globalThis.value = globalThis.value || [];
globalThis.value.push('abc');

对于这个输出,执行每个入口都会输出 ['ab', 'bc', 'abc']。然而,原始代码对每个入口输出的结果不同:

  • entry-a.js['ab', 'abc']
  • entry-b.js['ab', 'bc', 'abc']
  • entry-c.js['bc', 'abc']

关于如何解决这个问题,有一些讨论。一种方法是在原始顺序会被破坏时,将模块移动到额外的公共 chunk 中,但这可能会使输出碎片化。Rolldown 则提供了 strictExecutionOrder,它会包装 ESM 模块主体,使它们能够按照源码顺序运行,同时仍然保持 ESM 输出。当一个被包装的动态入口最终与其他代码共享其实现 chunk 时,重写后的 import() 会触发实现本身。严格模式只在仍然需要真实文件的地方发出一个很小的入口 facade —— 例如当另一个 chunk 静态加载该入口的 chunk 时,或者对于插件发出的 chunk —— 因此输出 chunk 的形状仍然可能发生变化。默认情况下,严格模式会包装每个符合条件的模块;实验性的 onDemandWrapping 模式则根据预测的 chunk 执行风险推导出一个保守的子集。

Was this page helpful?