ECMAScript – Introducing Dynamic Imports in ES2020 (ES11)

Published on March 8, 20214 min read

Introducing the “Dynamic Import” proposal, arriving with new import() keyword enabling to load a module on demand at runtime, which has been reached stage 4 in the TC39 process and is included in the language specification of 2020 – the 11th edition.

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:

The stages of TC39 process
The stages of 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:

Loading modules lazily based on hashtag
Loading modules lazily based on hashtag

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 module
  • import() can be invoked at any level of the file
  • import() allows to load a module on demand or conditionally at runtime
  • import() allows computing the module specifier at runtime
  • import() can be used inside regular <script> elements

Here’s the repository containing the code of the final example.

Follow Me

Join My Newsletter

Get updates and insights directly to your inbox.

Site Navigation


© 2024, Nitay Neeman. All rights reserved.

Licensed under CC BY 4.0. Sharing and adapting this work is permitted only with proper attribution.