Skip to content

Concept: load-time execution and call-time execution #5

Description

@hyfdev

Motivation

We need to distinguish these two concepts when we discuss the execution order of user output.

Load-time execution

Load-time execution is the code that runs automatically when a generated module or chunk is loaded. In ordinary output, it includes the user module bodies.

Consider two entries:

// entry-a.js
import './x.js'
import './y.js'
console.log(globalThis.order.join(','))

// entry-b.js
import './y.js'
import './x.js'
console.log(globalThis.order.join(','))

// x.js
globalThis.order ??= []
globalThis.order.push('x')

// y.js
globalThis.order ??= []
globalThis.order.push('y')

When each entry is evaluated as the initial entry in a fresh runtime, the source behavior is:

  • entry-a.js: x,y
  • entry-b.js: y,x

With default code splitting, x.js and y.js can be placed into one shared chunk. That chunk has only one render order. If Rolldown renders x.js before y.js, the simplified output is:

// shared.js
globalThis.order ??= []
globalThis.order.push('x')
globalThis.order.push('y')

// entry-a.js
import './shared.js'
console.log(globalThis.order.join(','))

// entry-b.js
import './shared.js'
console.log(globalThis.order.join(','))

Loading either entry now runs x.js and then y.js. entry-a.js remains correct, but entry-b.js is wrong. Rendering y.js before x.js reverses the problem.

This is the limit of load-time execution here: loading the shared chunk also executes all user module bodies in its chosen physical order.

Call-time execution

Call-time execution is not a JavaScript engine phase. It is a user-land mechanism generated by the bundler.

The shared chunk can instead register module initializers at load time and defer the user module bodies:

// shared.js
var init_x = __esmMin(() => {
  globalThis.order ??= []
  globalThis.order.push('x')
})

var init_y = __esmMin(() => {
  globalThis.order ??= []
  globalThis.order.push('y')
})

export { init_x, init_y }

// entry-a.js
import { init_x, init_y } from './shared.js'
init_x()
init_y()
console.log(globalThis.order.join(','))

// entry-b.js
import { init_x, init_y } from './shared.js'
init_y()
init_x()
console.log(globalThis.order.join(','))

Loading shared.js now only makes init_x and init_y available. The user module bodies run when the entry calls them, so each entry can preserve its own source order.

__esmMin also preserves module singleton behavior: if both entries run in the same runtime, x.js and y.js run only once, in the order of the entry that initializes them first. That matches ESM semantics; this example compares each entry's first evaluation.

By separating the time when modules are defined from the time when their bodies run, we can preserve execution-order semantics without requiring the shared chunk to choose one global module-body order. This is required for strictExecutionOrder.

Phase order

For one entry evaluation, the entry chunk and all of its static dependencies first complete load-time execution. In the lowered output above, that means all relevant initializer functions are defined before call-time execution begins.

The phases do not alternate per chunk. They are not load-time -> call-time -> load-time -> call-time -> ... along the static dependency graph.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions