import() in JavaScript: loading a module only when it is needed

import() loads a module during execution and returns a promise. It is the tool behind lazy loading and behind splitting an application into chunks.
3 min read
Believemy logo

An application often ships code that most visitors will never run: the rich text editor, the PDF generator, the admin view. All of it weighs on the first paint, for nothing.

The import() form lets you push that loading back to the exact moment the code becomes necessary.


Definition

A dynamic import is a function call, import(path), that loads a module during execution and returns a Promise. That promise fulfills with an object holding every export of the module, the default export included under the default key.

JAVASCRIPT
// cart.js
export const VAT = 0.2;
export default function totalWithTax(net) {
  return net * (1 + VAT);
}

// app.js
const cart = await import('./cart.js');

console.log(Object.keys(cart)); // [ 'VAT', 'default' ]
console.log(cart.default(100)); // 120

Destructuring makes the call easier to read: const { default: totalWithTax, VAT } = await import('./cart.js').


What lazy loading changes

A Bundler treats every import() as a boundary: the module and its dependencies go into a separate file, downloaded only when that line runs. This is code splitting, and it is often the most profitable performance win in a project.

JAVASCRIPT
button.addEventListener('click', async () => {
  const { openEditor } = await import('./editor.js');
  openEditor();
});

The same mechanism loads a translation file based on the chosen language, or a charting library only on the page that draws charts.

Good to know

Unlike a regular import, import() can be written anywhere: inside a condition, inside a function, inside an event handler. That is precisely what the static form forbids.


Three traps worth knowing

  • A fully variable path breaks analysis. Tools need to guess its shape: prefer import(`./locales/${code}.js`), whose fixed part stays readable, over a path built end to end.
  • The promise can be rejected. A missing file or a dropped connection causes a rejection, to be handled with try and catch or the interface simply freezes.
  • The module stays cached. Ten calls on the same path download the file once, so there is no point memoizing the result yourself.


Frequently asked questions

Question

Does import() always need await in front of it?

No, that is merely the most readable form. import('./x.js').then(...) does exactly the same job, and suits code that does not sit inside an async function. Inside an ES module, await is also accepted at the top level of the file.


Question

Can it be used from a CommonJS file?

Yes, and it is in fact the official bridge between the two worlds: import() works inside a CommonJS file and can load a ES module, where require() fails. The trade-off is that the result arrives asynchronously.


Question

When does splitting become counterproductive?

When the chunks get too small or too numerous: every separate file costs a request, and splitting a two kilobyte module earns nothing. Keep the technique for blocks that are genuinely heavy or genuinely rare, and measure before generalizing.

Related terms

Discover our javaScript glossary

Every word of JavaScript explained simply: keywords, built-in objects, methods, errors and concepts. Clear definitions and examples that actually run, to learn and to troubleshoot.

Share this article

Want to help us? Share this article on your networks or even better: on your site, in an article or in your newsletter.