1 of 25

Deferred import evaluation�for Stage 2.7

Nicolò Ribaudo (Igalia, in partnership with Bloomberg)

June 2024

2 of 25

The import defer proposal

import defer allows importing modules while deferring their evaluation until when it's actually needed.

2

3 of 25

The import defer proposal

import defer allows importing modules while deferring their evaluation until when it's actually needed.

import defer * as dep from "dep";

3

This doesn't execute dep

4 of 25

The import defer proposal

import defer allows importing modules while deferring their evaluation until when it's actually needed.

import defer * as dep from "dep";

onClick(() => {� console.log(dep.theAnswer);� });

4

It happens here!

This doesn't execute dep

5 of 25

The import defer proposal

import defer allows importing modules while deferring their evaluation until when it's actually needed.

import defer * as dep from "dep";

onClick(() => {� console.log(dep.theAnswer);� });

Why? We are looking at large mature codebases, where a significant part of the startup cost is module evaluation.

5

It happens here!

This doesn't execute dep

6 of 25

Top-level await

Modules using top-level await cannot be evaluated synchronously. What happens when a deferred module subgraph contains one of them?

  • Eager evaluation of async modules — the async subgraphs of a deferred subgraph are evaluated eagerly, as if they were not deferred

6

import defer is a best-effort optimization

7 of 25

Top-level await

7

1

1

2

4

3

await

This is our entrypoint

Assume then that something triggers the evaluation of this module

Already evaluated!

"Classic" import

Deferred import

Initial evaluation

Deferred evaluation

2

await

8 of 25

Top-level await

8

await

"Classic" import

Deferred import

main.js

a

b

c

d

e

f

import defer * as a from "a";

import "f";

is equivalent to

preload-and-link "a";

import "c";

import "f";

const a = new Proxy({}, {

get(_, key) {

const m = evaluateSync("a");

return m.[[Namespace]][key];

}

});

9 of 25

Top-level await

Discarded approaches

  • Disallow import defer of a module that has async dependencies

Problem: prevents from deferring a lot of work that could be deferred

  • Disallow import defer of a module that has yet-to-be-evaluated async dependencies

Problem: racy

9

10 of 25

Why not import()?

10

11 of 25

Why not dynamic import?

Dynamic import() is great!

onClick(async () => {

const dep = await import("dep");

console.log(dep.theAnswer);

});

11

This avoids any cost related to "dep" until when it's actually necessary.

12 of 25

Why not dynamic import?

Dynamic import() is great!

onClick(async () => {

const dep = await import("dep");

console.log(dep.theAnswer);

});

Problem: dynamic import makes your code asynchronous, and thus adopting it has high friction.

12

This avoids any cost related to "dep" until when it's actually necessary.

13 of 25

Why not dynamic import?

Dynamic import() is great!

onClick(async () => {

const dep = await import("dep");

console.log(dep.theAnswer);

});

Problem: dynamic import makes your code asynchronous, and thus adopting it has high friction.

We are looking for a solution that can be easily adopted at large scale.

13

This avoids any cost related to "dep" until when it's actually necessary.

14 of 25

Debugging experience

14

15 of 25

Debugging experience

Debugging module graphs is already complex:

  • Why is this module imported?
  • How much other modules is this library causing me to load?
  • Why is module A being executed before module B?
  • Is this module asynchronous?

15

16 of 25

Example: Webpack Visualizer

16

17 of 25

Example: deno info

deno info ./components/Icons.tsx

local:./components/Icons.tsx

type: TSX

dependencies: 7 unique

size: 108.72KB

file:///[...]/github.com/nicolo-ribaudo/train-trips/components/Icons.tsx (486B)

├─┬ https://esm.sh/preact@10.19.2/jsx-runtime (159B)

│ ├─┬ https://esm.sh/v128/preact@10.19.2/jsx-runtime/src/index.d.ts (1.52KB)

│ │ ├─┬ https://esm.sh/v128/preact@10.19.2/src/index.d.ts (10.76KB)

│ │ │ └─┬ https://esm.sh/v128/preact@10.19.2/src/jsx.d.ts (83.08KB)

│ │ │ └── https://esm.sh/v128/preact@10.19.2/src/index.d.ts *

│ │ └── https://esm.sh/v128/preact@10.19.2/src/jsx.d.ts *

│ ├── https://esm.sh/stable/preact@10.19.2/denonext/preact.mjs (10.83KB)

│ └─┬ https://esm.sh/stable/preact@10.19.2/denonext/jsx-runtime.js (1.82KB)

│ └── https://esm.sh/stable/preact@10.19.2/denonext/preact.mjs *

└─┬ https://esm.sh/preact@10.19.2 (90B)

├── https://esm.sh/v128/preact@10.19.2/src/index.d.ts *

└── https://esm.sh/stable/preact@10.19.2/denonext/preact.mjs *

17

18 of 25

Debugging experience

Debugging module graphs is already complex …� … and this proposal is making it even more complex.

  • How much time does evaluating this module take?
  • Is this import defer useful?
  • What is not being deferred?

18

There can be tools that help debugging these questions! Maybe built-in in browsers?

Example: https://github.com/nicolo-ribaudo/import-defer-polyfill

19 of 25

Changes since last presentation

19

20 of 25

Fixed a re-entrancy bug (#39 + partially #43)

The module evaluation algorithm relies on the invariant that its synchronous part is not re-entrant: when it starts, no modules in the to-be-evaluated graph can be in the EVALUATING state.

20

"Classic" import

Deferred import

b

c

d

a

⇒ Trying to evaluate a module that is being evaluated is an error.

Coordinated with Node.js for their require(esm) implementation.

evals ...c

21 of 25

Disallow reads from not-succesfully-eval'd modules (#43)

Problem: Reading from a namespace of a module that threw is currently allowed, but in practice incredibly rare to notice.

import defer * as ns from "./module-that-throws.js";

// ...

ns.foo;

Solution: Accessing properties from a namespace obtained through import defer throws if the module cannot be (or already is) successfully evaluated.

Consequence: There are two namespace objects per module.

import defer * as x1 from "x";

import * as x2 from "x";

x1 !== x2;

21

Will this throw?

22 of 25

Disallow reads from not-succesfully-eval'd modules (#43)

Problem: Reading from a namespace of a module that threw is currently allowed, but in practice incredibly rare to notice.

import defer * as ns from "./module-that-throws.js";

// ...

ns.foo;

Solution: Accessing properties from a namespace obtained through import defer throws if the module cannot be successfully evaluated (or already failed).

Consequence: There are two namespace objects per module.

import defer * as x1 from "x";

import * as x2 from "x";

x1 !== x2;

22

23 of 25

Disallow reads from not-succesfully-eval'd modules (#43)

Problem: Reading from a namespace of a module that threw is currently allowed, but in practice incredibly rare to notice.

import defer * as ns from "./module-that-throws.js";

// ...

ns.foo;

Solution: Accessing properties from a namespace obtained through import defer throws if the module cannot be successfully evaluated (or already failed).

Consequence: There are up to two namespace objects per module.

import defer * as x1 from "x";

import * as x2 from "x";

x1 !== x2;

23

24 of 25

Stage 2.7?

24

25 of 25

Stage 2.7

Complete spec text

Reviewers approved the spec text

🟡 ECMA-262 editors approved the spec text

25