1 of 55

Deferred Module Evaluation

TC39 January 2021,

Yulia Startsev

2 of 55

🇨🇦

3 of 55

“As performant as possible

under the circumstances”

4 of 55

What are the circumstances?

  • We are looking at a large, mature codebases, where a large part of the cost is module evaluation.
    • Alternatively: retain their original behaviour.
  • There is a need to make things more performant without making them async.
  • Users are ok sacrificing some performance for ergonomics and sync behaviour.

5 of 55

6 of 55

7 of 55

8 of 55

Is this a meaningful await?

9 of 55

Is there an alternative?

10 of 55

Maybe!

11 of 55

Goal:

Improve startup performance without sacrificing readability.

12 of 55

Goal:

Improve startup performance without sacrificing readability.

13 of 55

Goal:

Improve startup performance without sacrificing readability.

14 of 55

Deferring the module...

  • Before Load
  • Before Parse
  • Before Evaluate

15 of 55

Deferring the module...

  • Before Load
  • Before Parse
  • Before Evaluate

16 of 55

Deferring the module...

  • Before Load
  • Before Parse
  • Before Evaluate

17 of 55

Deferring the module...

  • Before Load
  • Before Parse
  • Before Evaluate

This.

18 of 55

Proposed API

alternatively...

19 of 55

Proposed API

alternatively...

Ignore this for now.

20 of 55

Proposed Semantics

  • Load and parse all of the modules.
  • Children of a deferred module are treated as part of the “deferred graph”
    • If a child is eagerly loaded elsewhere, it participates in the deferred module graph as a loaded module.
  • Only evaluate the module at first use

21 of 55

Simplified module graph

22 of 55

Left side of parent module is marked as lazy

{ lazyInit: true }

23 of 55

This creates a lazy subgraph, with eagerly loaded modules

24 of 55

Invariant of children finishing evaluation before parents remains.

“Lazy” module graph

25 of 55

Also applies to overlapping lazy/eager graphs

Still eager

26 of 55

This is consistent recursively. Green here is a lazy subgraph of a lazy graph

27 of 55

Known Issues

28 of 55

Top Level Await

Throw?

29 of 55

Top Level Await

Throw?

30 of 55

Alternative: Async is treated eagerly

async module

31 of 55

Side-effectful Get on Local Names

As Global Getter

Problem...

32 of 55

I/O costs - is this really a benefit

  • SSD vs HDD
  • Network vs local disk
  • Real world applications are doing lazy loading of applications, so there is some benefit.

33 of 55

Existing practice in frontend frameworks (link)

34 of 55

Code-splitting:

an important complementary tool

35 of 55

But… what about Suspense? (In repo)

36 of 55

Frontend code, Suspense, cont. (link)

37 of 55

Usability in client-side code

  • Two techniques which may make this realistic:
    • Resource bundles
    • HTTP/3 (or HTTP/2)
  • There is strong support for similar proposals, such as import maps
  • This could be a supporting proposal for a future story for imports client side.

38 of 55

Summary

  • Goal: Improve startup performance without sacrificing readability.
  • Solves the motivation through deferring evaluation of modules.
  • Requires further investigation regarding expected performance improvements.
  • Is a distinct use-case from dynamic import, but both are useful.
  • We need to address side-effectful code.

39 of 55

Requesting Stage 1 for

  • Investigating deferring module evaluation as a strategy for improving performance at startup.
  • With the API (pending further investigation) roughly defined as:

  • To be investigated in stage 1:
    • Verifying performance benefit
    • Best syntactic approach & solutions to the known issues
    • Other constraints raised by committee

40 of 55

Stage 1?

41 of 55

Question: How much startup time do we save?

42 of 55

Firefox Performance characteristics, ~45% spent loading/parsing (on SSD)

43 of 55

Firefox Performance characteristics ~54% spent running

44 of 55

Star configuration at time of profile

45 of 55

Question: Is “laziness” a feature of the edge or the node?

46 of 55

Shared lazy sub-graph, with module-edge level laziness

{ lazyInit: true }

47 of 55

What about a module that should always be lazy?

48 of 55

Marking a module as lazy within the module text (ie. a directive)

“use deferred-eval”

49 of 55

“use deferred-eval”

50 of 55

Shared lazy sub-graph, with module level laziness

“use deferred-eval”

51 of 55

Question: Can we do this with existing code? (link)

52 of 55

Question: Side-effects.

53 of 55

Side-effectful Get on Local Names

Throw?

54 of 55

Side-effectful Get on Local Names (link)

55 of 55

Stage 1?