Skip to main content

Dynamic import()

import() is a function that lets you import a module dynamically at runtime, rather than at page load time the way a regular import does. It returns a Promise that resolves to an object holding the module's exports.

Theory

TL;DR

  • import() loads a module at the moment it is called, not when the application starts.
  • It returns a Promise resolving to { default, ...namedExports }.
  • It shrinks the main bundle and speeds up the first load.
  • Bundlers move every dynamic import into its own chunk, which is code splitting.
  • It can be called anywhere: in a function, in a condition, in an event handler.
  • It is supported in every modern browser and in Node.js from version 14.

Quick example

javascript
// Static import, runs while the module is being loaded import { add } from './math.js'; // Dynamic import, runs when we call it const math = await import('./math.js'); console.log(math.add(2, 3));

A regular import is static: it loads immediately when the application starts, even if the module is never needed. import() is asynchronous: it loads the module only when that is genuinely required. As a result the main bundle gets smaller (together with code splitting), the initial page load gets faster, and code can be pulled in on demand.

Syntax, and what import() returns

javascript
import(modulePath) .then((module) => { // module is an object holding the exports module.default(); // if the export was default module.someFunction(); // if it was a named export }) .catch(console.error);

or with async/await:

javascript
async function loadMath() { const math = await import('./math.js'); console.log(math.add(2, 3)); }

import() returns a Promise that either:

  • resolves successfully and gives the module object { default, ...namedExports };
  • or rejects and lands in catch(), if the file was not found or the download failed.

Lazy loading and conditional imports

javascript
button.addEventListener('click', async () => { const { showModal } = await import('./modal.js'); showModal(); });

Here the modal.js module is loaded only on the click, not during the initial page load.

A conditional import works the same way:

javascript
if (user.isAdmin) { const admin = await import('./admin-panel.js'); admin.init(); }

The module loads only if it is needed, that is for administrators. Ordinary users never download the extra code.

The path can be built from a template string:

javascript
const moduleName = 'chart'; const module = await import(`./plugins/${moduleName}.js`);

But there is a catch: the path has to be known to the bundler in advance, otherwise it may leave the file out of the build. In practice the bundler takes the static part of the path (./plugins/) and adds every file in that folder to the bundle.

How it works in bundlers

In bundlers such as Webpack, Vite or Rollup, a call to import() automatically creates a separate chunk, that is a separate file.

For example:

javascript
import('./analytics.js');

creates a separate file analytics-XYZ.chunk.js, which is downloaded only when import() is called. That is exactly the code splitting mechanism: loading code dynamically.

Differences from a regular import

Propertyimport (static)import() (dynamic)
Syntaximport { x } from 'y'await import('y')
When it runsWhile the module is loadedAt runtime, when called
AsynchronySynchronous at build levelAsynchronous, a Promise
Where it is writtenAt the top of the fileAnywhere: in a function, in a condition
Used forThe main application codeLazy modules, code splitting

An example in React

jsx
import React, { lazy, Suspense } from 'react'; const Settings = lazy(() => import('./Settings')); export default function App() { return ( <Suspense fallback={<p>Loading...</p>}> <Settings /> </Suspense> ); }

Here React uses import() under the hood. The Settings component is loaded only when it is actually rendered.

Summary:

Key ideaMeaning
What it doesLoads a module asynchronously at runtime
What it returnsA Promise resolving to the exports object
What it is forLazy loading, code splitting, conditional imports
BenefitsFaster loading, a smaller bundle
CompatibilityEvery modern browser and Node.js 14+

Common mistakes

  • Forgetting that it is a Promise. const m = import('./a.js'); m.add(1, 2) does not work, because m is a Promise; you need await or .then().
  • Treating a default export as a named one. The module object keeps default as its own field, so a default export needs (await import('./a.js')).default.
  • Building the path entirely dynamically. The bundler cannot analyse await import(userInput); it needs at least a static base for the path.
  • Not handling a failed load. The network can drop, and without a catch() the user gets a blank screen instead of a message or a retry.
  • Splitting the code too finely. Dozens of micro-chunks mean dozens of requests, and the overhead eats the benefit of code splitting.
  • Lazy loading something needed immediately. If a module is required by the first screen, a dynamic import only adds latency instead of removing it.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.