ESC

Type to search the knowledge base.

Dynamic import()

import() returns a module namespace promise — code splitting, conditional loads, and error handling for lazy routes.

intermediate3 min read
  • javascript
  • dynamic-import
  • modules
  • code-splitting

Static import is hoisted and fixed at load time. import() is a function-like expression that loads a module on demand and returns a Promise of its namespace. Bundlers turn it into separate chunks — that’s route-based code splitting.

Basic form

const mod = await import('./utils/math.js');
mod.add(1, 2);

// named
const { add } = await import('./utils/math.js');

// default export lives on .default
const Chart = (await import('./Chart.js')).default;
import('./heavy.js')
  .then((m) => m.init())
  .catch((err) => console.error('chunk failed', err));

When to use it

Scenario Pattern
Route / page load view module on navigation
Rare UI (modal, admin) import on first open
Feature flag import only if enabled
Locale data import(./i18n/${locale}.js) — careful with bundler magic comments
button.addEventListener('click', async () => {
  const { openModal } = await import('./modal.js');
  openModal();
});

React.lazy style

// framework wraps import()
const Settings = React.lazy(() => import('./Settings.jsx'));

// bare equivalent
async function loadSettings() {
  const mod = await import('./Settings.jsx');
  return mod.default;
}

Always handle the loading and error states — network failures on chunks are real.

Paths and bundlers

// static string — best for bundler analysis
await import('./feature.js');

// dynamic path — bundler may include a whole directory context
const locale = 'en';
await import(`./messages/${locale}.json`);

Webpack/Vite use magic comments / glob rules. Fully dynamic import(userString) from arbitrary URLs is a security boundary — don’t load untrusted module URLs.

Caching

Modules are evaluated once per URL. Second import() of the same specifier resolves to the same exports (same module map entry).

const a = await import('./counter.js');
const b = await import('./counter.js');
a === b; // namespace objects may differ by identity in engines, but state is shared
a.inc === b.inc; // true — same binding

Top-level await in modules

Inside an ES module you can:

const config = await import('./config.js');
export const api = config.apiUrl;

That delays module evaluation (see top-level await).

Errors

try {
  await import('./missing.js');
} catch (e) {
  // network / 404 / syntax error in module
  showFallback();
}

Prefetch when you can predict the need:

link.addEventListener(
  'mouseenter',
  () => {
    import('./editor.js'); // warm cache
  },
  { once: true },
);

Interview answer

“import() dynamically loads an ES module and returns a Promise of its namespace. Bundlers split it into async chunks for lazy routes and heavy features. Specifiers should be statically analyzable when possible; repeated imports share the module instance. Always handle rejection for failed chunk loads.”

Named chunks and failure UX

const mod = await import(
  /* webpackChunkName: "editor" */
  './editor.js'
);

Vite/Rollup use different comment/annotation conventions — check your bundler. On failure, show a retry control; chunk 404s happen after deploys when users hold old HTML pointing at hashed files that were purged.

async function loadEditor() {
  try {
    return await import('./editor.js');
  } catch {
    toast('Failed to load editor — refresh?');
    throw new Error('chunk');
  }
}

Prefetch on hover/focus for primary CTAs so the click feels instant without loading the chunk on every page view.

Further reading

Related guides