Back to Rolldown

Automatic Code Splitting

docs/in-depth/automatic-code-splitting.md

1.2.112.5 KB
Original Source

Automatic Code Splitting

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

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.

dot
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

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:

::: code-group

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

:::

The following diagram shows how entries share dependencies and how modules are grouped into chunks:

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

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:

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:

::: code-group

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.

dot
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 move modules into additional common chunks whenever their original order would be violated, but that can fragment the output. Rolldown instead offers strictExecutionOrder, which wraps ESM module bodies so they can run in source order while keeping ESM output. When a wrapped dynamic entry ends up sharing its implementation chunk with other code, the rewritten import() triggers the implementation itself. Strict mode emits a small entry facade only where a real file is still needed — for example when another chunk statically loads the entry's chunk, or for chunks a plugin emits — so the output chunk shape can still change. By default strict mode wraps every eligible module; the experimental onDemandWrapping mode derives a conservative subset from predicted chunk execution hazards.