docs/in-depth/automatic-code-splitting.md
Automatic code splitting is the process of creating chunks from modules. This chapter describes its behavior and the principles behind it.
Automatic code splitting is not controllable. It runs following certain rules. Thus, we will also refer to it as automatic code splitting against manual code splitting done by manual code splitting.
Two types of chunks are generated by automatic code splitting.
Entry chunks are generated by combining modules connected statically into a chunk. "statically" means static import ... from '...' or require(...).
There are two types of entry chunks.
The first one is initial chunks. Initial chunks are generated due to users' configuration. For example, input: ['./a.js', './b.js'] defines two initial chunks.
The second one is dynamic chunks. Dynamic chunks are generated due to dynamic imports. Dynamic imports are used to load code on demand, so we don't put imported code together with the importers.
For the following code, two chunks will be generated:
// entry.js (included in `input` option)
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';
In this case, there are two groups of statically connected modules.
digraph {
bgcolor="transparent";
rankdir=LR;
node [shape=box, style="filled,rounded", fontname="Arial", fontsize=12, margin="0.2,0.1", color="${#3c3c43|#dfdfd6}", fontcolor="${#3c3c43|#dfdfd6}"];
edge [fontname="Arial", fontsize=10, color="${#3c3c43|#dfdfd6}", fontcolor="${#3c3c43|#dfdfd6}"];
subgraph cluster_group1 {
label="Group 1 (initial chunk)";
labeljust="l";
fontname="Arial";
fontsize=11;
fontcolor="${#3c3c43|#dfdfd6}";
style="dashed,rounded";
color="${#d44803|#ff712a}";
entry [label="entry.js", fillcolor="${#fff0e0|#4a2a0a}"];
foo [label="foo.js", fillcolor="${#fff0e0|#4a2a0a}"];
entry -> foo [label="static import"];
}
subgraph cluster_group2 {
label="Group 2 (dynamic chunk)";
labeljust="l";
fontname="Arial";
fontsize=11;
fontcolor="${#3c3c43|#dfdfd6}";
style="dashed,rounded";
color="${#0366d6|#58a6ff}";
dyn [label="dyn-entry.js", fillcolor="${#dbeafe|#1e3a5f}"];
bar [label="bar.js", fillcolor="${#dbeafe|#1e3a5f}"];
dyn -> bar [label="require()"];
}
entry -> dyn [label="import()", style=dashed];
}
Since there are two groups, in the end, automatic code splitting will generate two chunks.
Common chunks are generated when a module gets statically imported by at least two different entries. Those modules are put into a separate chunk.
The purpose of this behavior is:
It is important to note that whether a module could be put into the same common chunk is determined by if it is imported by the same entries.
For the following code, six chunks will be generated:
// entry-a.js (included in `input` option)
import 'shared-by-ab.js';
import 'shared-by-abc.js';
console.log(globalThis.value);
// entry-b.js (included in `input` option)
import 'shared-by-ab.js';
import 'shared-by-bc.js';
import 'shared-by-abc.js';
console.log(globalThis.value);
// entry-c.js (included in `input` option)
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');
The chunks will be generated as follows:
::: code-group
import './common-ab.js';
import './common-abc.js';
import './common-ab.js';
import './common-bc.js';
import './common-abc.js';
import './common-bc.js';
import './common-abc.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');
:::
The following diagram shows how entries share dependencies and how modules are grouped into chunks:
digraph {
bgcolor="transparent";
rankdir=TB;
node [shape=box, style="filled,rounded", fontname="Arial", fontsize=12, margin="0.2,0.1", color="${#3c3c43|#dfdfd6}", fontcolor="${#3c3c43|#dfdfd6}"];
edge [fontname="Arial", fontsize=10, color="${#3c3c43|#dfdfd6}", fontcolor="${#3c3c43|#dfdfd6}"];
newrank=true;
// Entry nodes
entry_a [label="entry-a.js", fillcolor="${#fff0e0|#4a2a0a}"];
entry_b [label="entry-b.js", fillcolor="${#fff0e0|#4a2a0a}"];
entry_c [label="entry-c.js", fillcolor="${#fff0e0|#4a2a0a}"];
// Shared module nodes
shared_ab [label="shared-by-ab.js", fillcolor="${#dbeafe|#1e3a5f}"];
shared_bc [label="shared-by-bc.js", fillcolor="${#dbeafe|#1e3a5f}"];
shared_abc [label="shared-by-abc.js", fillcolor="${#e0e7ff|#2e1065}"];
// Edges
entry_a -> shared_ab;
entry_a -> shared_abc;
entry_b -> shared_ab;
entry_b -> shared_bc;
entry_b -> shared_abc;
entry_c -> shared_bc;
entry_c -> shared_abc;
// Chunk grouping
subgraph cluster_chunk_a {
label="entry-a.js chunk";
labeljust="l";
fontname="Arial";
fontsize=11;
fontcolor="${#3c3c43|#dfdfd6}";
style="dashed,rounded";
color="${#d44803|#ff712a}";
entry_a;
}
subgraph cluster_chunk_b {
label="entry-b.js chunk";
labeljust="l";
fontname="Arial";
fontsize=11;
fontcolor="${#3c3c43|#dfdfd6}";
style="dashed,rounded";
color="${#d44803|#ff712a}";
entry_b;
}
subgraph cluster_chunk_c {
label="entry-c.js chunk";
labeljust="l";
fontname="Arial";
fontsize=11;
fontcolor="${#3c3c43|#dfdfd6}";
style="dashed,rounded";
color="${#d44803|#ff712a}";
entry_c;
}
subgraph cluster_common_ab {
label="common-ab.js chunk";
labeljust="l";
fontname="Arial";
fontsize=11;
fontcolor="${#3c3c43|#dfdfd6}";
style="dashed,rounded";
color="${#0366d6|#58a6ff}";
shared_ab;
}
subgraph cluster_common_bc {
label="common-bc.js chunk";
labeljust="l";
fontname="Arial";
fontsize=11;
fontcolor="${#3c3c43|#dfdfd6}";
style="dashed,rounded";
color="${#0366d6|#58a6ff}";
shared_bc;
}
subgraph cluster_common_abc {
label="common-abc.js chunk";
labeljust="l";
fontname="Arial";
fontsize=11;
fontcolor="${#3c3c43|#dfdfd6}";
style="dashed,rounded";
color="${#0366d6|#58a6ff}";
shared_abc;
}
}
entry-*.js chunks are generated by the reason discussed above. common-*.js chunks are the common chunks. These are created because:
common-ab.js: shared-by-ab.js is imported by both entry-a.js and entry-b.js.common-bc.js: shared-by-bc.js is imported by both entry-b.js and entry-c.js.common-abc.js: shared-by-abc.js is imported by all 3 entries.You may ask why automatic code splitting doesn't place shared-by-*.js files into a single common chunk. The reason is that doing so would violate the original code's intention.
For the example above, if a single common chunk were created, it will be like:
globalThis.value = globalThis.value || [];
globalThis.value.push('ab');
globalThis.value = globalThis.value || [];
globalThis.value.push('bc');
globalThis.value = globalThis.value || [];
globalThis.value.push('abc');
For this output, executing each entry will output ['ab', 'bc', 'abc']. However, the original code outputs a different result for each entry:
entry-a.js: ['ab', 'abc']entry-b.js: ['ab', 'bc', 'abc']entry-c.js: ['bc', 'abc']Rolldown tries to place your modules in the order declared in the original code.
For the following code:
// entry.js
import { foo } from './foo.js';
console.log(foo);
// foo.js
export var foo = 'foo';
Rolldown will try to calculate the order by emulating the execution, starting from entries.
In this case, the execution order is [foo.js, entry.js]. So the bundle output will be like:
// foo.js
var foo = 'foo';
// entry.js
console.log(foo);
However, Rolldown sometimes places modules without respecting their original order. This is because ensuring that modules are singletons takes precedence over placing them in the declared order.
For the following code:
// entry.js (included in `input` option)
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';
The bundle output will be:
::: code-group
import './common-execution.js';
// setup.js
globalThis.value = 'hello, world';
import './common-execution.js';
console.log(globalThis.value);
:::
common-execution.js is a common chunk. It is generated because execution.js is imported by both entry.js and dyn-entry.js.
digraph {
bgcolor="transparent";
rankdir=TB;
node [shape=box, style="filled,rounded", fontname="Arial", fontsize=12, margin="0.2,0.1", color="${#3c3c43|#dfdfd6}", fontcolor="${#3c3c43|#dfdfd6}"];
edge [fontname="Arial", fontsize=10, color="${#3c3c43|#dfdfd6}", fontcolor="${#3c3c43|#dfdfd6}"];
compound=true;
subgraph cluster_entry {
label="entry.js chunk";
labeljust="l";
fontname="Arial";
fontsize=11;
fontcolor="${#3c3c43|#dfdfd6}";
style="dashed,rounded";
color="${#d44803|#ff712a}";
entry [label="entry.js", fillcolor="${#fff0e0|#4a2a0a}"];
setup [label="setup.js", fillcolor="${#fff0e0|#4a2a0a}"];
}
subgraph cluster_dyn {
label="dyn-entry.js chunk";
labeljust="l";
fontname="Arial";
fontsize=11;
fontcolor="${#3c3c43|#dfdfd6}";
style="dashed,rounded";
color="${#d44803|#ff712a}";
dyn [label="dyn-entry.js", fillcolor="${#fff0e0|#4a2a0a}"];
}
subgraph cluster_common {
label="common-execution.js chunk";
labeljust="l";
fontname="Arial";
fontsize=11;
fontcolor="${#3c3c43|#dfdfd6}";
style="dashed,rounded";
color="${#0366d6|#58a6ff}";
execution [label="execution.js", fillcolor="${#dbeafe|#1e3a5f}"];
}
entry -> setup [label="import"];
entry -> execution [label="import"];
entry -> dyn [label="import()", style=dashed];
dyn -> execution [label="import"];
}
This example shows the problem, before bundling, the code outputs hello, world, but after bundling, it outputs undefined. Currently, there's no easy way to solve this problem, as well for other bundlers that output ESM.
::: info Related issues for other bundlers
:::
There are some discussions on how to solve this problem. One way is to generate more common chunks once a module violates its original order. But this will generate more common chunks, which is not a good idea. Rolldown tries to solve this issue by strictExecutionOrder, which injects some helper code to ensure the execution order is respected while keeping esm output and avoiding additional common chunks.