Every year, an edition of the ECMAScript Language Specification is released with the new proposals that are officially ready. In practical terms, the proposals are attached to the latest expected edition when they are accepted and reached stage 4 in the TC39 process:
In this article, we’re going to examine and explain the "Dynamic Import" proposal that has been reached stage 4 and belongs to ECMAScript 2020 – the 11th edition.
The content is available as a video as well:
Motivation
Modules are merely different files containing split scripts including variables and functionality – instead of having them in a single tedious file. Technically, it’s being done by exporting the module features using the export
statement and importing them into other files using the import
statement.
When using the import
statement that arrived with ES2015, the code is loaded statically – which means the exported module code is evaluated up front at load-time, before that the importer code is executed. This way is preferable when possible to load the initial modules and to allow static analysis including tree shaking.
That said, sometimes we might want to load a module on demand at runtime – such as when the module isn’t really needed for loading, the import specifier string should be computed dynamically, loading the module from a regular <script>
element and such.
So, let’s explain how we actually can make those possible based on the Dynamic Import proposal.
The Proposal
The proposal specifies a new syntactic keyword, import()
, that can be invoked dynamically from the code – and that’s why it’s also named "Dynamic Import".
Let’s see the official definition of the specification:
The process of a dynamic import originally started by an import() call, resolving or rejecting the promise returned by that call as appropriate according to completion.
From the definition we directly understand that the import()
keyword acting like a function that returns a promise.
In detail, the function-like receives a specifier of a module to load and the returned promise resolves into an object containing all the exports of the module.
Note: The keyword isn’t a real function but rather a convenient syntactic form looking like one, which means it doesn’t inherit from Function.prototype
and methods such as call
or apply
aren’t supported (and needed).
Loading at Runtime
ES2015 introduced the static import
statement allowing to load exports from a module by a string literal representing its specifier:
import { myFunction } from './module.mjs';
myFunction();
Clearly, assuming the functionality is truly exported using the export
statement:
export const myFunction = () => console.log('This is an exported function!');
This works since when we import any exports from an external module – it’s actually attached into the current scope via a pre-runtime "linking" process.
Although static importing is probably the way to go in most cases, it only can be used at the top level of the file. In other words, it doesn’t allow to import the module on demand or conditionally.
Well, this is the part that Dynamic Imports come into the picture – because the new import()
keyword can be invoked at any level of the file:
// Static imports
// import { ... } from '...';
// ...
import('./module.mjs').then((module) => {
// The exports are accessible through the module object
module.myFunction();
});
As said, the promise resolves into a module object containing its exports at runtime – so we can simply invoke our exported function down the file.
And going forward, the real power is the ability to load the code lazily on demand:
button.addEventListener('click', () =>
import('./module.mjs').then((module) => {
module.myFunction();
})
);
In the example we import the module only after a button is clicked. This absolutely might boost the load-time performance in case the module isn’t needed for loading but rather only after some trigger occurs.
Another thing to mention is that compared to static imports, we definitely can involve async
functions to make the syntax cleaner:
button.addEventListener('click', async () => {
const { myFunction } = await import('./module.mjs');
myFunction();
});
More than that, it’s also possible to condition the loading:
if (isFeatureEnabled) {
import('./module.mjs').then((module) => {
module.myFunction();
});
}
We can make the module be loaded only whether some condition is true, for example – if a feature is enabled by the user, a polyfill is necessary on legacy platforms or just when it brings heavy side-effects.
Dynamic Module Specifier
We already said that the static import
statement expects a string literal of the module specifier. This naturally means that the module specifier is fixed.
In contrast, the new import()
keyword allows computing the module specifier dynamically:
window.addEventListener('hashchange', async () => {
// Extracts the new route
const route = window.location.hash.substr(1);
// Loads the module based on the route
const { myFunction } = await import(`./${route}.mjs`);
myFunction();
});
Basically in that example we simulate a router loading the modules lazily based on URL hashtag change events. Once hashchange
is fired, we extract the new "route" followed by the hashtag. Then, we simply resolve from it the module specifier and pass the computed value to the import
keyword.
Note: We assumed there are two module files corresponding to the hashtags.
Using Script Element
So far we could combine the <script>
element with type="module"
in order to use static import inside to load as top-level modules. In practice, type="module"
differentiates those scripts from regular scripts and instructs the browser that they are modules – which should be evaluated only once.
But now, with the new import()
keyword, we can load modules inside regular scripts in simplicity:
<body>
<a href="#module1">Module 1</a>
<a href="#module2">Module 2</a>
<script>
window.addEventListener('hashchange', async () => {
const route = window.location.hash.substr(1);
const { myFunction } = await import(`./${route}.mjs`);
myFunction();
});
</script>
</body>
Here’s how it looks:
Summary
We covered today the motivation behind the “Dynamic Import” proposal and explained the possible abilities through concrete examples.
Let’s recap:
- The proposal belongs to ECMAScript 2020, which is the 11th edition
- When using the
import
statement, the code is evaluated statically up front at load-time – before that the importer code is executed - When using the
import
statement, the module specifier is fixed - The proposal specifies a new syntactic keyword written
import()
import()
acting like a function that returns a promise resolves into an object containing all its the exports of the moduleimport()
can be invoked at any level of the fileimport()
allows to load a module on demand or conditionally at runtimeimport()
allows computing the module specifier at runtimeimport()
can be used inside regular<script>
elements
Here’s the repository containing the code of the final example.