Post cover

ES Modules Dynamic Import

Updated March 22, 2023

ES modules are a way to organize cohesive chunks of code in JavaScript. Here's a simple ES module:

// An ES module
import { concat } from './concatModule.js';
concat('a', 'b'); // => 'ab'

import { concat } from './concatModule.js' is considered a static import.

Static importing works in most situations. But sometimes to save client's bandwidth you may choose to load the modules dynamically.

You can import ES modules dynamically if you use import as a function — import(pathToModule) — a feature available starting ES2020.

Let's see how ES modules' dynamic import works, and when it's useful.

1. Dynamic importing

When the import keyword is used as a function:

const module = await import(path);

import(path) returns a promise and starts an asynchronous task to load the module located at path. If the module is loaded successfully, then the promise resolves to the module content, otherwise, the promise rejects.

path can be any expression that evaluates to a string denoting a path. Valid path expressions are:

// Classic string literals
const module1 = await import('./myModule.js');
// A variable
const path = './myOtherModule.js';
const module2 = await import(path);
// Function call
const getPath = (version) => `./myModule/versions/${version}.js`;
const moduleVersion1 = await import(getPath('v1.0'));
const moduleVersion2 = await import(getPath('v2.0'));

import(path), returning a promise, works great with the async/await syntax. For example, let's load a module inside of an asynchronous function:

async function loadMyModule() {
const myModule = await import('./myModule.js');
// ... use myModule

Now, knowing how to load the module, let's extract components (default or named) from the imported module.

2. Importing components

2.1 Dynamic import of named

Let's consider the following module, named namedConcat.js:

// namedConcat.js
export const concat = (paramA, paramB) => paramA + paramB;

namedConcat performs a named export of concat function.

To dynamically import namedConcat.js, and access the named export concat, then destructure the resolved module object by the named export:

async function loadMyModule() {
const { concat } = await import('./namedConcat.js');
concat('b', 'c'); // => 'bc'

2.2 Dynamic import of default

To dynamically import a default, just read the default property from the module object.

Let's say that defaultConcat.js exports the function as a default export:

// defaultConcat.js
export default (paramA, paramB) => paramA + paramB;

When importing defaultConcat.js dynamically, and specifically accessing the default export, just read the default property.

But there's a nuance. default is a keyword in JavaScript, so it cannot be used as a variable name. What you do is use destructuring with aliasing:

async function loadMyModule() {
const { default: defaultFunc } = await import('./defaultConcat.js');
defaultFunc('b', 'c'); // => 'bc'

2.3 Dynamic import of mixed content

If the imported module exports default and multiple named exports, then you can access all these components using a single destructuring:

async function loadMyModule() {
const {
default: defaultImport,
} = await import('./mixedExportModule.js');
// ...

3. When to use dynamic import

I recommend using dynamic import when importing big modules conditionally:

  • you might use the module from time to time, depending on runtime conditions
  • you might want to load different versions of a big module, also depending on runtime conditions.

For example:

async function execBigModule(condition) {
if (condition) {
const { funcA } = await import('./bigModuleA.js');
} else {
const { funcB } = await import('./bigModuleB.js');

For small modules (like namedConcat.js or defaultConcat.js from the previous example), that have a few lines of code, the dynamic import doesn't worth the hassle.

4. Conclusion

To load dynamically a module call import(path) as a function with an argument indicating the specifier (aka path) to a module.

const module = await import(path) returns a promise that resolves to an object containing the components of the imported module.

In that object, the default property contains the default export, and the named exports are contained in the corresponding properties:

const {
default: defaultComponent,
} = await import(path);

The dynamic import is supported by both Node.js (version 13.2 and above) and most modern browsers.

What other interesting use cases of the dynamic import do you know? Share your idea in a comment below!

Like the post? Please share!

Dmitri Pavlutin

About Dmitri Pavlutin

Software developer and sometimes writer. My daily routine consists of (but not limited to) drinking coffee, coding, writing, overcoming boredom 😉. Living in the sunny Barcelona. 🇪🇸