Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 0 additions & 22 deletions docs/bundler/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -562,28 +562,6 @@ Each `import()` of a bundled JavaScript module also becomes its own chunk. Tree

With `target: "bun"`, a `require()` of a bundled ES module becomes a chunk of its own as well. The call is emitted as `import.meta.require("./chunk-<hash>.js")` and stays synchronous — Bun evaluates the chunk when the call runs — so a `require()` inside a function that never runs keeps its module out of the startup working set entirely. Tree shaking treats these chunks like `import()` chunks. `require()` of a CommonJS module is unaffected and keeps returning `module.exports`. A `require()` cycle between such chunks follows Bun's runtime `require()` of an ES module: a module required again while its chunk is still being loaded sees the partially initialized CommonJS placeholder (`{}`) rather than the live bindings the in-chunk wrapper provides. For other targets — and with `splitRequire: false` (`--no-split-require`) — the required module is inlined into the calling chunk behind a lazy wrapper instead, because the call must return synchronously.

#### Evaluation order of shared modules

Modules that the same entrypoints load go into one shared chunk, and a chunk runs its modules in one order. Each entrypoint is a program of its own, so two entrypoints can import the same modules in different orders:

<CodeGroup>

```ts index.ts icon="/icons/typescript.svg"
import "./a.ts";
import "./b.ts";
```

```ts worker.ts icon="/icons/typescript.svg"
import "./b.ts";
import "./a.ts";
```

</CodeGroup>

When `a.ts` and `b.ts` have side effects, Bun writes them to separate chunks, and each entrypoint imports the chunks in its own order. Modules that every entrypoint runs one after the other, in the same order, still share a chunk. So do modules that only declare, whatever the order of the imports, modules in an import cycle, modules that reach a top-level `await`, and modules that import a CommonJS module.

The same applies to two `import()` targets: the target that loads first decides the order, as it does when the source runs unbundled. When both calls are in flight at once, the host decides which target evaluates first. If the order matters, `await` the first `import()` before you start the second.

#### Tree-shaking `import()` and `require()` results

When the result of a string-literal `import()` or `require()` of an ES module is only ever used to read specific exports, the exports nobody reads are dropped from that module — and whatever only they depended on tree-shakes with them. This applies with and without `splitting`; the module is still loaded lazily, exactly where the call is written.
Expand Down
5 changes: 1 addition & 4 deletions src/bundler/Chunk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,7 @@ bitflags::bitflags! {
const IS_EXECUTABLE = 1 << 0;
const HAS_HTML_CHUNK = 1 << 1;
const IS_BROWSER_CHUNK_FROM_SERVER_BUILD = 1 << 2;
/// The file of this entry point is in another chunk, and the entry point loads a chunk that
/// `split_chunks_by_evaluation_order` made.
const RANKS_IMPORTS_FROM_ENTRY_POINT_FILE = 1 << 3;
// _padding: u4 = 0
// _padding: u5 = 0
}
}

Expand Down
2 changes: 0 additions & 2 deletions src/bundler/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,6 @@ pub mod linker_context {

#[path = "mergeSmallChunks.rs"]
pub mod merge_small_chunks;
#[path = "splitChunksByEvaluationOrder.rs"]
pub(crate) mod split_chunks_by_evaluation_order;

#[path = "crossChunkNames.rs"]
pub mod cross_chunk_names;
Expand Down
13 changes: 0 additions & 13 deletions src/bundler/linker_context/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -745,19 +745,6 @@ The renamed symbols are then used during final code generation to produce output
- Keeps a chunk out of the fold when it can be in the middle of being evaluated while an entry of its class loads (it, or a file that statically imports its way to it, `require()`s a split ES module): the entry's chunk reads the other members then
- With `--min-chunk-size`, additionally folds small chunks with no top-level side effects into a chunk loaded by a superset of their entries when every dependency is already loaded wherever the target is, no static import cycle between chunks results, and every CommonJS/ESM wrapper the moved code initializes at the top level is already initialized by a chunk the target imports

#### `splitChunksByEvaluationOrder.rs`

**Purpose**: Keeps files out of one chunk when the entry points that load them run them in different orders. A chunk prints its files in one order, and each chunk imports the other chunks in its own order. Runs after `mergeSmallChunks.rs`, on the entry bits that it leaves.

**Key functions**:

- Groups the files by chunk key (`File.entry_bits`). Only the entry points that can be the first to load a group count (`EntryLoadGraph::load_class`)
- Only a file that runs something when it loads has an order that can be observed (`loading_file_has_no_side_effects`), and a file of the group that imports such a file
- A group with such a file in an import cycle stays as it is: chunks import each other for the bindings that they use, which in a cycle is not the order in which the files run. So does a group with such a file that reaches a top-level await: a chunk does not wait for a chunk that it does not import. And a group with such a file that imports a wrapped file: the call of the wrapper prints where the `import` is, between the files that the other imports run
- A group that a split `require()` loads stays as it is: the imports of a chunk are not ranked by what the call runs
- Walks the imports of each such entry point (`for_each_edge`), in parallel, for the order in which it evaluates those files
- Files that every order has back to back, in the same order, are a run. `computeChunks()` adds the number of the run to the chunk key when a group has several. The other files of the group keep the key as it is

#### `computeCrossChunkDependencies.rs`

**Purpose**: Resolves dependencies between different chunks.
Expand Down
34 changes: 2 additions & 32 deletions src/bundler/linker_context/computeChunks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ use super::find_all_imported_parts_in_js_order::find_all_imported_parts_in_js_or
use super::find_imported_css_files_in_js_order::find_imported_css_files_in_js_order;
use super::find_imported_files_in_css_order::find_imported_files_in_css_order;
use super::merge_small_chunks::merge_small_chunks;
use super::split_chunks_by_evaluation_order::split_chunks_by_evaluation_order;

#[inline(always)]
fn make_flags(has_html_chunk: bool, is_browser_chunk_from_server_build: bool) -> chunk::Flags {
Expand Down Expand Up @@ -282,30 +281,6 @@ pub(crate) fn compute_chunks(
let min_chunk_size = this.options.min_chunk_size;
merge_small_chunks(this, temp, min_chunk_size)?;
}
let run_of_file: Vec<u32> = if code_splitting {
split_chunks_by_evaluation_order(this)?
} else {
Vec::new()
};
if !run_of_file.is_empty() {
let file_entry_bits = this.graph.files.items_entry_bits();
let mut loads_split_off_chunk = AutoBitSet::init_empty(this.graph.entry_points.len())?;
for (source_index, _) in run_of_file.iter().enumerate().filter(|(_, run)| **run != 0) {
loads_split_off_chunk.set_union(&file_entry_bits[source_index]);
}
let entry_source_indices = this.graph.entry_points.items_source_index();
for (entry_id, &source_index) in entry_source_indices.iter().enumerate() {
let chunk_index = entry_point_to_js_chunk_idx[entry_id];
if loads_split_off_chunk.is_set(entry_id)
&& file_entry_bits[source_index as usize].count() >= 2
&& chunk_index != u32::MAX
{
js_chunks.values_mut()[chunk_index as usize]
.flags
.insert(chunk::Flags::RANKS_IMPORTS_FROM_ENTRY_POINT_FILE);
}
}
}
let css_asts = this.graph.ast.items_css();
let ast_targets = this.graph.ast.items_target();

Expand Down Expand Up @@ -352,13 +327,8 @@ pub(crate) fn compute_chunks(
if !contributes_code.is_set(source_index.get() as usize) {
continue;
}
let entry_bits_key = entry_bits.bytes(this.graph.entry_points.len());
let js_chunk_key: &[u8] = match run_of_file.get(source_index.get() as usize)
{
None | Some(0) => temp.alloc_slice_copy(entry_bits_key),
Some(run) => temp
.alloc_slice_copy(&[entry_bits_key, &run.to_le_bytes()].concat()),
};
let js_chunk_key =
temp.alloc_slice_copy(entry_bits.bytes(this.graph.entry_points.len()));
let js_chunk_entry = js_chunks.get_or_put(js_chunk_key)?;

if !js_chunk_entry.found_existing {
Expand Down
21 changes: 2 additions & 19 deletions src/bundler/linker_context/findAllImportedPartsInJSOrder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,14 +266,6 @@ impl WalkPlan {
plan.chunk_of_file[file] = chunk_index;
}
}
// Several chunks can have one key (`split_chunks_by_evaluation_order`).
for (chunk_index, chunk) in chunks.iter().enumerate() {
if matches!(chunk.content, chunk::Content::Javascript(_)) {
for &file in chunk.files_with_parts_in_chunk.keys() {
plan.chunk_of_file[file as usize] = chunk_index as u32;
}
}
}
}

// The entry point that loads first among the chunk's entry points owns the chunk.
Expand Down Expand Up @@ -331,7 +323,7 @@ impl WalkPlan {
}

#[derive(Clone, Copy)]
pub(crate) enum Edge {
enum Edge {
/// `file` runs here, under the same load.
Import(IndexInt),
/// A split `require()` in a part that runs at load: the chunk of `file` runs here.
Expand All @@ -342,7 +334,7 @@ pub(crate) enum Edge {

/// The files that a file leads to, in evaluation order, with the part that leads there. `runs`: the load evaluates the file.
/// A file runs where it is imported, not where its bindings are used: `part.dependencies` does not order what runs.
pub(crate) fn for_each_edge(
fn for_each_edge(
c: &LinkerContext,
source_index: IndexInt,
runs: bool,
Expand Down Expand Up @@ -856,13 +848,6 @@ fn reached_chunks_in_order(
// Start where the load enters this chunk.
let mut roots: Vec<IndexInt> = chunk.files_with_parts_in_chunk.keys().to_vec();
roots.sort_unstable_by_key(|&source_index| order.entered[source_index as usize]);
// The load enters at the file of the entry point, which is in another chunk.
if chunk
.flags
.contains(chunk::Flags::RANKS_IMPORTS_FROM_ENTRY_POINT_FILE)
{
roots.insert(0, chunk.entry_point.source_index());
}

let mut reached: Vec<u32> = Vec::new();
let mut reached_set = AutoBitSet::init_empty(chunks_len)?;
Expand Down Expand Up @@ -896,9 +881,7 @@ fn reached_chunks_in_order(
visited.set(source_index as usize);

let is_file_in_chunk = if css[source_index as usize].is_none() {
// Several chunks can have one key (`split_chunks_by_evaluation_order`).
entry_bits.eql(&file_entry_bits[source_index as usize])
&& matches!(chunk_of_file[source_index as usize], other if other == chunk_index || other == u32::MAX)
} else {
entry_bits.has_intersection(&file_entry_bits[source_index as usize])
};
Expand Down
Loading
Loading