At Bloomberg, we have been using lazy module evaluation in production for a long time. We have proved multiple times that it's essential for better application startup performance. Hence we are very happy to see this proposal and will invest in its standardization.
This proposal, as it stands, is generally inline with our view. Please see our detailed feedback below.
Existing Use Cases
Our module loader speaks AMD. Historically, AMD has also been the authoring syntax for modules. This has changed in the last few years. Modules are now authored in ESM syntax by default and AMD is considered legacy as an authoring format. ES modules can interoperate with AMD and vice versa. In terms of interop, it would be fair to consider an AMD module as an ES module with a single default export.
Currently we have two primary features that give users lazy evaluation capabilities. These are designed to be used more in the producer modules when building module exports.
In AMD, a programmatic API lets users create objects with getter functions as a proxy to other modules, i.e. re-exports.
In ESM, a compiler-based feature that makes all re-exports of a module lazy. This feature is heavily restricted.
Both of these are used in critical parts of the infrastructure. They don't take special care for asynchronous modules, i.e. ESM using top-level await, or AMD using a special async formulation. Users are expected to create safe dependency graphs by knowing what children to preload ahead of time. This makes the feature very brittle, which we are actively working to address (see below).
1. Default-exported Object with Lazy Properties
In this pattern, the default-exported object contains getter functions that load different target modules on first access.
Using the current proposal, this can be implemented as follows:
import * as Label from "./label" with { lazyInit: true };
import * as Button from "./button" with { lazyInit: true };
export default {
get Label() { return Label.default },
get Button() { return Button.default },
};
This requires .default for each namespace which is a little inconvenient.
2. Lazy Namespace of (Lazy) Namespaces
In this pattern, a mega lazy namespace is constructed by re-exporting other namespaces. A special pragma tells the build tooling to transform eager static re-exports to lazy evaluation.
/* "use special pragma to make re-exports lazy at build time" */
export * as TableUtils from "./table-utils";
export * as WindowUtils from "./window-utils";
This is possible to implement using the current proposal as it's allowed to pass the DeferredModuleNamespace exotic object around without breaking its lazy nature.
import * as TableUtils from "./table-utils" with { lazyInit: true };
import * as WindowUtils from "./window-utils" with { lazyInit: true };
export { TableUtils, WindowUtils };
It cannot be expressed concisely only using re-export statements, though.
3. Lazy Namespace of Named Re-exports
The same pragma can be used to construct a namespace where only selected named bindings are re-exported lazily. Although this pattern is supported, we don't have many users of it today.
/* "use special pragma to make re-exports lazy at build time" */
export { Table } from "./table";
export { Window } from "./window";
This cannot be expressed in the current proposal as it only allows namespace imports and there is no way to export a function that works like a getter.
Implementation of Deferred Module Evaluation
We have implemented deferred module evaluation in our module system. It is now used by a few early adopters in production. As background, this module system is not known to the JS engine. It is almost entirely implemented in JS and is constructed during an initial bootstrapping phase that runs before the main application module is loaded.
Most of the complexity we found when implementing this feature arises due to handling of asynchronous modules in the graph. Supporting top-level await is important in our system, which is why we have invested in standardization of it in the past. So this was a critical requirement for us. Handling of asynchronous modules is achieved by a combination of metadata collected at build time and preloading asynchronous nodes in the runtime. Build-time metadata is reliable because we enforce static analyzability of dependency graphs. This metadata enables an optimization that makes the preloading traversal in the runtime more efficient by avoiding parsing modules which makes discovery of asynchronous modules possible without the need to explore the whole static module graph.
We are aligning with the current state of the proposal as much as we can by
using a comment based syntax in import declarations with a goal of moving to Import Attributes and eventually to the final standard syntax
only allowing namespace imports - named imports are banned
At Bloomberg, we have been using lazy module evaluation in production for a long time. We have proved multiple times that it's essential for better application startup performance. Hence we are very happy to see this proposal and will invest in its standardization.
This proposal, as it stands, is generally inline with our view. Please see our detailed feedback below.
Existing Use Cases
Our module loader speaks AMD. Historically, AMD has also been the authoring syntax for modules. This has changed in the last few years. Modules are now authored in ESM syntax by default and AMD is considered legacy as an authoring format. ES modules can interoperate with AMD and vice versa. In terms of interop, it would be fair to consider an AMD module as an ES module with a single default export.
Currently we have two primary features that give users lazy evaluation capabilities. These are designed to be used more in the producer modules when building module exports.
Both of these are used in critical parts of the infrastructure. They don't take special care for asynchronous modules, i.e. ESM using top-level await, or AMD using a special async formulation. Users are expected to create safe dependency graphs by knowing what children to preload ahead of time. This makes the feature very brittle, which we are actively working to address (see below).
1. Default-exported Object with Lazy Properties
In this pattern, the default-exported object contains getter functions that load different target modules on first access.
Using the current proposal, this can be implemented as follows:
This requires
.default
for each namespace which is a little inconvenient.2. Lazy Namespace of (Lazy) Namespaces
In this pattern, a mega lazy namespace is constructed by re-exporting other namespaces. A special pragma tells the build tooling to transform eager static re-exports to lazy evaluation.
This is possible to implement using the current proposal as it's allowed to pass the
DeferredModuleNamespace
exotic object around without breaking its lazy nature.It cannot be expressed concisely only using re-export statements, though.
3. Lazy Namespace of Named Re-exports
The same pragma can be used to construct a namespace where only selected named bindings are re-exported lazily. Although this pattern is supported, we don't have many users of it today.
This cannot be expressed in the current proposal as it only allows namespace imports and there is no way to export a function that works like a getter.
Implementation of Deferred Module Evaluation
We have implemented deferred module evaluation in our module system. It is now used by a few early adopters in production. As background, this module system is not known to the JS engine. It is almost entirely implemented in JS and is constructed during an initial bootstrapping phase that runs before the main application module is loaded.
Most of the complexity we found when implementing this feature arises due to handling of asynchronous modules in the graph. Supporting top-level await is important in our system, which is why we have invested in standardization of it in the past. So this was a critical requirement for us. Handling of asynchronous modules is achieved by a combination of metadata collected at build time and preloading asynchronous nodes in the runtime. Build-time metadata is reliable because we enforce static analyzability of dependency graphs. This metadata enables an optimization that makes the preloading traversal in the runtime more efficient by avoiding parsing modules which makes discovery of asynchronous modules possible without the need to explore the whole static module graph.
We are aligning with the current state of the proposal as much as we can by