JS: Module. Dynamic Import

By Xah Lee. Date: . Last updated: .

(new in ECMAScript 2020)

What is Dynamic Import

Dynamic import is introduced in ECMAScript 2020. It let you import a module at run-time. You can import a module by condition inside a if-statement, and module name can also be a variable.

Import and export features of ECMAScript 2015 are static. That means, the code runs at compile time. You cannot import at run time, such as import a lib only when needed.

whats JavaScript Dynamic Import

whats JavaScript Dynamic Import

(ai answer)

JavaScript dynamic import (import()) is a function-like expression that loads an ES module asynchronously at runtime, instead of at parse/load time like a static import declaration.

It returns a Promise that resolves to a module namespace object (all of that module’s exports). import here is a keyword, not a normal function.

Syntax

import(moduleSpecifier);
import(moduleSpecifier, options);
const module = await import("./utils.js");
module.doStuff();

const data = await import("./data.json", { with: { type: "json" } });

Static vs dynamic import

Static import Dynamic import()
When it runs Load time (before your module executes) When the expression runs
Where it can appear Top level of a module only Anywhere (functions, if, event handlers)
Specifier String literal only Computed string OK
Return value Bindings in scope Promise → namespace object
Needs type="module" Yes (in browsers) No — can be used from classic scripts too

Static imports are analyzed and linked before execution. Dynamic imports fetch, instantiate, and evaluate the module only when you call import().

Why use it

Examples

Promise style

button.addEventListener("click", () => {
 import("./greeting.js")
  .then((mod) => {
   mod.sayHi();
  })
  .catch((err) => {
   console.error("Failed to load module", err);
  });
});

async/await

button.addEventListener("click", async () => {
 try {
  const { sayHi } = await import("./greeting.js");
  sayHi();
 } catch (err) {
  console.error(err);
 }
});

Conditional

if (needsHeavyFeature) {
 const { run } = await import("./heavy-feature.js");
 run();
}

The resolved object is a namespace: default export is module.default, named exports are properties. Bindings stay live (same as static imports). Modules are cached for the lifetime of the page/worker — a second import() of the same specifier does not re-fetch/re-evaluate.

Support

Widely available in browsers since ~2020 (Chrome 63+, Firefox 67+, Safari 11.1+) and in Node.js since 13.2. It is a baseline web feature.

Use static imports by default (simpler, better for tree-shaking and static analysis). Reach for import() when you actually need runtime or on-demand loading.

xtodo

(ai answer)

JavaScript Dynamic Imports: A Complete Tutorial

Dynamic imports (also known as import() expressions) allow you to load JavaScript modules on demand at runtime, rather than statically at the top of your file.


Why Use Dynamic Imports?

Static import (eager):

import HeavyLibrary from "./heavy-library.js"; // Loaded immediately

Dynamic import (lazy):

const button = document.getElementById("load-chart");
button.addEventListener("click", async () => {
 const { default: HeavyLibrary } = await import("./heavy-library.js");
 // Now use it
});

Basic Syntax

The import() function returns a Promise that resolves to the module namespace object.

import(moduleSpecifier)
 .then((module) => {
  // module.default, module.namedExport, etc.
 })
 .catch((err) => console.error(err));

With async/await (recommended):

async function loadModule() {
 try {
  const module = await import("./my-module.js");
  console.log(module);
 } catch (err) {
  console.error("Failed to load module:", err);
 }
}

Module Specifier

The argument to import() can be:


Working with Exports

Default Export

// math.js
export default function add(a, b) {
 return a + b;
}
// main.js
const { default: add } = await import("./math.js");
// or
const math = await import("./math.js");
const result = math.default(2, 3);

Named Exports

// utils.js
export const PI = 3.14;
export function square(x) {
 return x * x;
}
const utils = await import("./utils.js");
console.log(utils.PI);
console.log(utils.square(5));

You can also use Assignment pattern directly:

const { PI, square } = await import("./utils.js");

Real-World Examples

Feature Flags

async function initializeApp() {
 if (userHasPremium) {
  const { PremiumFeatures } = await import("./premium.js");
  PremiumFeatures.init();
 }
}

3. Route-based Code Splitting

const routes = {
 "/dashboard": () => import("./pages/Dashboard.js"),
 "/settings": () => import("./pages/Settings.js"),
};

async function navigate(path) {
 const module = await routes[path]();
 // render module.default or similar
}

Error Handling

Always wrap dynamic imports in try/catch:

async function safeImport(path) {
 try {
  return await import(path);
 } catch (error) {
  if (error.name === "TypeError") {
   console.error("Module loading failed - network or syntax error");
  } else {
   console.error("Unexpected error:", error);
  }
  // Fallback UI or retry logic
 }
}

Advanced Topics

Importing JSON (modern browsers)

const data = await import("./config.json", { assert: { type: "json" } });
// or in newer syntax:
const data = await import("./config.json", { with: { type: "json" } });

Dynamic Import in CommonJS (require)

Dynamic imports work in both ESM and CommonJS modules.

Caching

Modules loaded with import() are cached just like static imports. Subsequent calls to the same specifier return the cached module instantly.

const m1 = await import("./foo.js");
const m2 = await import("./foo.js"); // Returns cached module

To force a reload (rarely needed), use cache-busting query params:

await import(`./foo.js?v=${Date.now()}`);

Browser and Environment Support

Polyfill for very old browsers (not recommended for new projects):

<script nomodule src="dynamic-import-polyfill.js"></script>

Best Practices

  1. Use async/await for readability.
  2. Keep the import call close to where the module is used.
  3. Avoid putting dynamic imports in the top level of modules unless necessary (can delay module evaluation).
  4. Preload critical chunks using <link rel="modulepreload"> for better performance.
  5. Handle loading states (spinners, skeletons) in UI.
  6. Monitor bundle size — dynamic imports create new network requests.

Common Pitfalls


Comparison: Static vs Dynamic

Feature Static import Dynamic import()
Loading time Build time / initial Runtime (on demand)
Bundle splitting Limited Excellent
Tree shaking Full Full
Conditional loading Not possible Perfect
Top-level syntax Yes Function call