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']

模块放置顺序

Rolldown 会尝试按照原始代码中声明的顺序放置你的模块。

对于以下代码:

js
// entry.js
import { foo } from './foo.js';
console.log(foo);

// foo.js
export var foo = 'foo';

Rolldown 会尝试从入口开始,通过模拟执行来计算顺序。

在这种情况下,执行顺序是 [foo.js, entry.js]。因此 bundle 输出将如下所示:

output.js
js
// foo.js
var foo = 'foo';

// entry.js
console.log(foo);

尊重执行顺序并不优先

不过,Rolldown 有时会在不遵守原始顺序的情况下放置模块。这是因为确保模块是单例的优先级高于按声明顺序放置模块。

对于以下代码:

js
// entry.js (包含在 `input` 选项中)
import './setup.js';
import './execution.js';

import('./dyn-entry.js');

// setup.js
globalThis.value = 'hello, world';

// execution.js
console.log(globalThis.value);

// dyn-entry.js
import './execution.js';

bundle 输出将为:

js
import './common-execution.js';

// setup.js
globalThis.value = 'hello, world';
js
import './common-execution.js';
js
console.log(globalThis.value);

common-execution.js 是一个公共 chunk。它之所以生成,是因为 execution.js 同时被 entry.jsdyn-entry.js 导入。

cluster_entryentry.js chunkcluster_dyndyn-entry.js chunkcluster_commoncommon-execution.js chunkentryentry.jssetupsetup.jsentry->setup导入dyndyn-entry.jsentry->dynimport()executionexecution.jsentry->execution导入dyn->execution导入
cluster_entryentry.js chunkcluster_dyndyn-entry.js chunkcluster_commoncommon-execution.js chunkentryentry.jssetupsetup.jsentry->setup导入dyndyn-entry.jsentry->dynimport()executionexecution.jsentry->execution导入dyn->execution导入

这个例子展示了问题:在打包之前,代码输出 hello, world,但在打包之后,它输出 undefined。目前没有简单的方法来解决这个问题,其他输出 ESM 的打包器也是如此。

其他打包器的相关问题

关于如何解决这个问题,已经有一些讨论。一种方法是,一旦某个模块违反了原始顺序,就生成更多的公共 chunk。但这会生成更多的公共 chunk,这并不是一个好主意。Rolldown 通过 strictExecutionOrder 来尝试解决这个问题,它会注入一些辅助代码,以确保在保持 esm 输出并避免额外公共 chunk 的同时,执行顺序仍然被遵守。