• Stars
    star
    208
  • Rank 189,015 (Top 4 %)
  • Language
    HTML
  • License
    MIT License
  • Created almost 4 years ago
  • Updated 5 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A proposal for introducing a way to defer evaluate of a module

Deferring Module Evaluation

previously known as "Lazy Module Initialization"

Status

Champion(s): Yulia Startsev

Author(s): Yulia Startsev and Guy Bedford

Stage: 1

Slides:

Background

JS applications can get very large, to the point that not only loading, but even executing their initialization scripts incurs a significant performance cost. Usually, this happens later in an application's life span - often requiring invasive changes to make it more performant.

Loading performance is a big and important area for improvement, and involves preloading techniques for avoiding waterfalls and dynamic import() for lazily loading modules.

But even with loading performance solved using these techniques, there is still overhead for execution performance - CPU bottlenecks during initialization due to the way that the code itself is written.

Motivation

Avoiding unnecessary execution is a well-known optimization in the Node.js CommonJS module system, where there is a smaller gap between load contention and execution contention. The common pattern in Node.js applications is to refactor code to dynamically require as needed:

const operation = require('operation');

exports.doSomething = function (target) {
  return operation(target);
}

being rewritten as a performance optimization into:

exports.doSomething = function (target) {
  const operation = require('operation');
  return operation(target);
}

The consumer still is provided with the same API, but with a more efficient use of FS & CPU during initialization time.

For ES modules, we have a solution for the lazy loading component of this problem via dynamic import().

For the same example we can write:

export async function doSomething (target) {
  const { operation } = await import('operations');
  return operation(target);
}

This avoids bottlenecking the network and CPU during application initialization, but there are still a number of problems with this technique:

  1. It doesn't actually solve the deferral of execution problem, since sending a network request in such a scenario would usually be a performance regression and not an improvement. A separate network preloading step would therefore still be desirable to achieve efficient deferred execution while avoiding triggering a waterfall of requests.

  2. It forces all functions and their callers into an asynchronous programming model, without necessarily reflecting the real intention of the program. This leads to all call sites having to be updated into a new model, and cannot be made without a breaking API change to existing API consumers.

Problem Statement

Deferring the synchronous evaluation of a module may be desirable new primitive to avoid unnecessary CPU work during application initialization, without requiring any changes from a module API consumer perspective.

Dynamic import does not properly solve this problem, since it must often be coupled with a preload step, and enforces the unnecessary asyncification of all functions, without providing the ability to only defer the synchronous evaluation work.

Proposal

The proposal is to have a new syntactical import form which will only ever return a namespace exotic object. When used, the module and its dependencies would not be executed, but would be fully loaded to the point of being execution-ready before the module graph is considered loaded.

Only when accessing a property of this module, would the execution operations be performed (if needed).

This way, the module namespace exotic object acts like a proxy to the evaluation of the module, effectively with [[Get]] behavior that triggers synchronous evaluation before returning the defined bindings.

The API will use the below syntax, following the phases model established by the source phase imports proposal:

// or with a custom keyword:
import defer * as yNamespace from "y";

Semantics

The imports would still participate in deep graph loading so that they are fully populated into the module cache prior to execution, however it the imported module will not be evaluated yet.

When a property of the resulting module namespace object is accessed, if the execution has not already been performed, a new top-level execution would be initiated for that module.

In this way, a deferred module evaluation import acts as a new top-level execution node in the execution graph, just like a dynamic import does, except executing synchronously.

There are possible extensions under consideration, such as deferred re-exports, but they are not included in the current version of the proposal.

Top-level await

Property access on the namespace object of a deferred module must be synchronous, and it's thus impossible to defer evaluation of modules that use top-level await. When a module is imported using the import defer syntax, its asynchronous dependencies together with their own transitive dependencies are eagerly evaluated, and only the synchronous parts of the graph are deferred.

Consider the following example, where a is the top-level entry point:

// a
import "b";
import defer * as c from "c"

setTimeout(() => {
  c.value
}, 1000);
// b
// c
import "d"
import "f"
export let value = 2;
// d
import "e"
await 0;
// e
// f

Since d uses top-level await, d and its dependencies cannot be deferred:

  • The initial evaluation will execute b, e, d and a.
  • Later, the c.value access will trigger the execution of f and c.

Rough sketch

If we split out the components of Module loading and initialization, we could roughly sketch out the intended semantics:

⚠️ The following example does not take cycles into account

// LazyModuleLoader.js
async function loadModuleAndDependencies(name) {
  const loadedModule = await import.load(`./${name}.js`); // load is async, and needs to be awaited
  const parsedModule = loadedModule.parse();
  await Promise.all(parsedModule.imports.map(loadModuleAndDependencies)); // load all dependencies
  return parsedModule;
}

async function executeAsyncSubgraphs(module) {
  if (module.hasTLA) return module.evaluate();
  return Promise.all(module.importedModules.map(executeAsyncSubgraphs));
}

export default async function lazyModule(object, name) {
  const module = await loadModuleAndDependencies(name);
  await executeAsyncSubgraphs(module);
  Object.defineProperty(object, name, {
    get: function() {
      delete object[name];
      const value = module.evaluateSync();
      Object.defineProperty(object, name, {
        value,
        writable: true,
        configurable: true,
        enumerable: true,
      });
      return value;
    },
    configurable: true,
    enumerable: true,
  });

  return object;
}

// myModule.js
import foo from "./bar";

etc.

// module.js
import LazyModule from "./LazyModuleLoader";
await LazyModule(globalThis, "myModule");

function Foo() {
  myModule.doWork() // first use
}

Implementations

Q&A

What happened to the direct lazy bindings?

The initial version of this proposal included direct binding access for deferred evaluation via named exports:

import { feature } from './lib' with { lazyInit: true }

export function doSomething (param) {
  return feature(param);
}

where the deferred evaluation would only happen on access of the feature binding.

There are a number of complexities to this approach, as it introduces a novel type of execution point in the language, which would need to be worked through.

This approach may still be investigated in various ways within this proposal or an extension of it, but by focusing on the module namespace exotic object approach first, it keeps the semantics simple and in-line with standard JS techniques.

Is there really a benefit to optimizing execution, when surely loading is the bottleneck?

While it is true that loading time is the most dominant factor on the web, it is important to consider that many large applications can block the CPU for of the range of 100ms while initializing the main application graph.

Loading times of the order of multiple seconds often take the focus for performance optimization work, and this is certainly an important problem space, but the problem of freeing up the main event loop during initialization remains a critical one when the network problem is solved, that doesn't currently have any easy solutions today for large applications.

Is there prior art for this in other languages?

The standard libraries of these programming languages includes related functionality:

  • Ruby's autoload, in contrast with require which works in the same way as JS import
  • Clojure import
  • Most LISP environments

Our approach is pretty similar to the Emacs Lisp approach, and it's clear from a manual analysis of billions of Stack Overflow posts that this is the most straightforward to ordinary developers.

Why not support a synchronous evaluation API on ModuleInstance

A synchronous evaluation API on the module expression and compartments ModuleInstance object could offer an API for synchronous evaluation of modules, which could be compatible with this approach of deferred evaluation, but it is only in having a clear syntactical solution for this use case, that it can be supported across dependency boundaries and in bundlers to bring the full benefits of avoiding unnecessary initialization work to the wider JS ecosystem.

What can we do in current JS to approximate this behavior?

The closest we can get is the following:

// moduleWrapper.js
export default function ModuleWrapper(object, name, lambda) {
  Object.defineProperty(object, name, {
    get: function() {
      // Redefine this accessor property as a data property.
      // Delete it first, to rule out "too much recursion" in case object is
      // a proxy whose defineProperty handler might unwittingly trigger this
      // getter again.
      delete object[name];
      const value = lambda.apply(object);
      Object.defineProperty(object, name, {
        value,
        writable: true,
        configurable: true,
        enumerable: true,
      });
      return value;
    },
    configurable: true,
    enumerable: true,
  });
  return object;
}

// module.js
import ModuleWrapper from "./ModuleWrapper";
// any imports would need to be wrapped as well

function MyModule() {
 // ... all of the work of the module
}

export default ModuleWrapper({}, "MyModule", MyModule);

// parent.js
import wrappedModule from "./module";

function Foo() {
  wrappedModule.MyModule.bar() // first use
}

However, this solution doesn't cover deferring the loading of submodules of a lazy graph, and would not acheive the characteristics we are looking for.

More Repositories

1

proposals

Tracking ECMAScript Proposals
17,177
star
2

ecma262

Status, process, and documents for ECMA-262
HTML
14,437
star
3

proposal-pipeline-operator

A proposal for adding a useful pipe operator to JavaScript.
HTML
7,534
star
4

proposal-pattern-matching

Pattern matching syntax for ECMAScript
HTML
5,498
star
5

proposal-optional-chaining

HTML
4,942
star
6

proposal-type-annotations

ECMAScript proposal for type syntax that is erased - Stage 1
JavaScript
4,252
star
7

proposal-signals

A proposal to add signals to JavaScript.
3,387
star
8

proposal-temporal

Provides standard objects and functions for working with dates and times.
HTML
3,321
star
9

proposal-observable

Observables for ECMAScript
JavaScript
3,058
star
10

proposal-decorators

Decorators for ES6 classes
2,640
star
11

proposal-record-tuple

ECMAScript proposal for the Record and Tuple value types. | Stage 2: it will change!
HTML
2,496
star
12

test262

Official ECMAScript Conformance Test Suite
JavaScript
2,073
star
13

proposal-dynamic-import

import() proposal for JavaScript
HTML
1,863
star
14

proposal-bind-operator

This-Binding Syntax for ECMAScript
1,742
star
15

proposal-class-fields

Orthogonally-informed combination of public and private fields proposals
HTML
1,722
star
16

proposal-async-await

Async/await for ECMAScript
HTML
1,578
star
17

proposal-object-rest-spread

Rest/Spread Properties for ECMAScript
HTML
1,493
star
18

proposal-shadowrealm

ECMAScript Proposal, specs, and reference implementation for Realms
HTML
1,429
star
19

proposal-iterator-helpers

Methods for working with iterators in ECMAScript
HTML
1,307
star
20

proposal-nullish-coalescing

Nullish coalescing proposal x ?? y
HTML
1,232
star
21

proposal-top-level-await

top-level `await` proposal for ECMAScript (stage 4)
HTML
1,083
star
22

proposal-partial-application

Proposal to add partial application to ECMAScript
HTML
1,002
star
23

proposal-do-expressions

Proposal for `do` expressions
HTML
990
star
24

proposal-binary-ast

Binary AST proposal for ECMAScript
961
star
25

agendas

TC39 meeting agendas
JavaScript
952
star
26

proposal-built-in-modules

HTML
891
star
27

proposal-async-iteration

Asynchronous iteration for JavaScript
HTML
857
star
28

proposal-explicit-resource-management

ECMAScript Explicit Resource Management
JavaScript
746
star
29

proposal-set-methods

Proposal for new Set methods in JS
HTML
655
star
30

proposal-string-dedent

TC39 Proposal to remove common leading indentation from multiline template strings
HTML
614
star
31

proposal-operator-overloading

JavaScript
610
star
32

proposal-import-attributes

Proposal for syntax to import ES modules with assertions
HTML
591
star
33

proposal-async-context

Async Context for JavaScript
HTML
587
star
34

proposal-bigint

Arbitrary precision integers in JavaScript
HTML
561
star
35

ecmascript_simd

SIMD numeric type for EcmaScript
JavaScript
540
star
36

ecma402

Status, process, and documents for ECMA 402
HTML
529
star
37

proposal-slice-notation

HTML
523
star
38

proposal-change-array-by-copy

Provides additional methods on Array.prototype and TypedArray.prototype to enable changes on the array by returning a new copy of it with the change.
HTML
511
star
39

notes

TC39 meeting notes
JavaScript
496
star
40

proposal-class-public-fields

Stage 2 proposal for public class fields in ECMAScript
HTML
489
star
41

proposal-iterator.range

A proposal for ECMAScript to add a built-in Iterator.range()
HTML
483
star
42

proposal-decimal

Built-in exact decimal numbers for JavaScript
HTML
477
star
43

proposal-uuid

UUID proposal for ECMAScript (Stage 1)
JavaScript
463
star
44

proposal-module-expressions

HTML
433
star
45

proposal-throw-expressions

Proposal for ECMAScript 'throw' expressions
JavaScript
425
star
46

proposal-UnambiguousJavaScriptGrammar

413
star
47

proposal-weakrefs

WeakRefs
HTML
409
star
48

proposal-array-grouping

A proposal to make grouping of array items easier
HTML
407
star
49

proposal-error-cause

TC39 proposal for accumulating errors
HTML
380
star
50

proposal-cancelable-promises

Former home of the now-withdrawn cancelable promises proposal for JavaScript
Shell
376
star
51

proposal-ecmascript-sharedmem

Shared memory and atomics for ECMAscript
HTML
374
star
52

proposal-module-declarations

JavaScript Module Declarations
HTML
369
star
53

proposal-first-class-protocols

a proposal to bring protocol-based interfaces to ECMAScript users
352
star
54

proposal-relative-indexing-method

A TC39 proposal to add an .at() method to all the basic indexable classes (Array, String, TypedArray)
HTML
351
star
55

proposal-global

ECMAScript Proposal, specs, and reference implementation for `global`
HTML
346
star
56

proposal-private-methods

Private methods and getter/setters for ES6 classes
HTML
345
star
57

proposal-numeric-separator

A proposal to add numeric literal separators in JavaScript.
HTML
330
star
58

proposal-private-fields

A Private Fields Proposal for ECMAScript
HTML
319
star
59

tc39.github.io

Get involved in specifying JavaScript
HTML
318
star
60

proposal-object-from-entries

TC39 proposal for Object.fromEntries
HTML
318
star
61

proposal-promise-allSettled

ECMAScript Proposal, specs, and reference implementation for Promise.allSettled
HTML
314
star
62

proposal-await.ops

Introduce await.all / await.race / await.allSettled / await.any to simplify the usage of Promises
HTML
310
star
63

proposal-regex-escaping

Proposal for investigating RegExp escaping for the ECMAScript standard
JavaScript
309
star
64

proposal-export-default-from

Proposal to add `export v from "mod";` to ECMAScript.
HTML
306
star
65

proposal-logical-assignment

A proposal to combine Logical Operators and Assignment Expressions
HTML
302
star
66

proposal-promise-finally

ECMAScript Proposal, specs, and reference implementation for Promise.prototype.finally
HTML
279
star
67

proposal-json-modules

Proposal to import JSON files as modules
HTML
272
star
68

proposal-asset-references

Proposal to ECMAScript to add first-class location references relative to a module
270
star
69

proposal-cancellation

Proposal for a Cancellation API for ECMAScript
HTML
267
star
70

proposal-promise-with-resolvers

HTML
255
star
71

proposal-string-replaceall

ECMAScript proposal: String.prototype.replaceAll
HTML
253
star
72

proposal-export-ns-from

Proposal to add `export * as ns from "mod";` to ECMAScript.
HTML
242
star
73

proposal-structs

JavaScript Structs: Fixed Layout Objects
230
star
74

proposal-ses

Draft proposal for SES (Secure EcmaScript)
HTML
223
star
75

proposal-intl-relative-time

`Intl.RelativeTimeFormat` specification [draft]
HTML
215
star
76

proposal-json-parse-with-source

Proposal for extending JSON.parse to expose input source text.
HTML
214
star
77

proposal-flatMap

proposal for flatten and flatMap on arrays
HTML
214
star
78

ecmarkup

An HTML superset/Markdown subset source format for ECMAScript and related specifications
TypeScript
201
star
79

proposal-promise-any

ECMAScript proposal: Promise.any
HTML
200
star
80

proposal-optional-chaining-assignment

`a?.b = c` proposal
186
star
81

proposal-decorators-previous

Decorators for ECMAScript
HTML
184
star
82

proposal-smart-pipelines

Old archived draft proposal for smart pipelines. Go to the new Hack-pipes proposal at js-choi/proposal-hack-pipes.
HTML
181
star
83

proposal-array-from-async

Draft specification for a proposed Array.fromAsync method in JavaScript.
HTML
178
star
84

proposal-upsert

ECMAScript Proposal, specs, and reference implementation for Map.prototype.upsert
HTML
176
star
85

proposal-collection-methods

HTML
171
star
86

proposal-array-filtering

A proposal to make filtering arrays easier
HTML
171
star
87

proposal-ptc-syntax

Discussion and specification for an explicit syntactic opt-in for Tail Calls.
HTML
169
star
88

proposal-extractors

Extractors for ECMAScript
JavaScript
166
star
89

proposal-error-stacks

ECMAScript Proposal, specs, and reference implementation for Error.prototype.stack / System.getStack
HTML
166
star
90

proposal-intl-duration-format

164
star
91

how-we-work

Documentation of how TC39 operates and how to participate
161
star
92

proposal-Array.prototype.includes

Spec, tests, reference implementation, and docs for ESnext-track Array.prototype.includes
HTML
157
star
93

proposal-promise-try

ECMAScript Proposal, specs, and reference implementation for Promise.try
HTML
154
star
94

proposal-extensions

Extensions proposal for ECMAScript
HTML
150
star
95

proposal-hashbang

#! for JS
HTML
148
star
96

proposal-import-meta

import.meta proposal for JavaScript
HTML
146
star
97

proposal-intl-segmenter

Unicode text segmentation for ECMAScript
HTML
146
star
98

proposal-resizablearraybuffer

Proposal for resizable array buffers
HTML
145
star
99

proposal-seeded-random

Proposal for an options argument to be added to JS's Math.random() function, and some options to start it with.
HTML
143
star
100

eshost

A uniform wrapper around a multitude of ECMAScript hosts. CLI: https://github.com/bterlson/eshost-cli
JavaScript
142
star