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
// 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
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:
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
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:
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:
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:
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
| Property | import (static) | import() (dynamic) |
|---|---|---|
| Syntax | import { x } from 'y' | await import('y') |
| When it runs | While the module is loaded | At runtime, when called |
| Asynchrony | Synchronous at build level | Asynchronous, a Promise |
| Where it is written | At the top of the file | Anywhere: in a function, in a condition |
| Used for | The main application code | Lazy modules, code splitting |
An example in React
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 idea | Meaning |
|---|---|
| What it does | Loads a module asynchronously at runtime |
| What it returns | A Promise resolving to the exports object |
| What it is for | Lazy loading, code splitting, conditional imports |
| Benefits | Faster loading, a smaller bundle |
| Compatibility | Every 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, becausemis a Promise; you needawaitor.then(). - Treating a default export as a named one. The module object keeps
defaultas 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 readyA concise answer to help you respond confidently on this topic during an interview.