Skip to content

Code Splitting

Code splitting is the process of creating chunks from modules. This chapter describes its behavior and the principles behind it.

Code splitting is not controllable. It runs following certain rules. Thus, we will also refer to it as automatic chunking against manual chunking done by advancedChunks.

Two types of chunks are generated by code splitting.

Entry chunks

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:

js
// 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.

  • Group 1: entry.js and foo.js
  • Group 2: dyn-entry.js and bar.js

Since there are two groups, in the end, automatic chunking will generate two chunks.

Common 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:

  • Ensure every JavaScript module is singleton in the final bundle output.
  • When a entry gets executed, only imported modules should get executed.

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:

js
// 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:

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');

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 chunking 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:

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');

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

Module Placing Order

Rolldown tries to place your modules in the order declared in the original code.

For the following code:

js
// 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:

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

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

Respecting Execution Order doesn't take precedence

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:

js
// 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:

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

// setup.js
globalThis.value = 'hello, world';
js
import './common-execution.js';
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.

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.

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 InputOptions#experimental#strictExecutionOrder, which injects some helper code to ensure the execution order is respected while keeping esm output and avoiding additional common chunks.

Released under the MIT License.