CodingNic

Modules & Code Organization

Dynamic Imports

Modules & Code Organization 15 min read

Dynamic Imports

Objectives

By the end of this chapter, you should be able to:

  • Explain the difference between a static import and a dynamic import()
  • Load a module on demand with import()
  • Read a dynamically imported module’s default and named exports

💡 Why this matters: Every import so far loads its module immediately, as soon as the page loads. Some code only needs to run occasionally, a rarely used feature, something behind a button click. Loading it upfront wastes bandwidth on code most visitors never trigger.

Static Import vs. Dynamic Import

Every import from earlier lessons is static: it has to sit at the top level of a file, and the module it points to loads immediately, before any of the file’s own code runs.

import(), written as a function call, is different. It can go anywhere in your code, runs whenever that line actually executes, and returns a promise that resolves to the module.

javascript
// heavyFeature.js
export default function runHeavyFeature() {
  return "Heavy feature ran";
}

export const VERSION = "1.0";
javascript
async function loadFeature() {
  const module = await import("./heavyFeature.js");
  console.log(module.default());
  console.log(module.VERSION);
}

loadFeature();
// Heavy feature ran
// 1.0

heavyFeature.js isn’t loaded until loadFeature() actually runs, not when the page first loads. The resolved module object holds every export, the default export is on module.default, named exports are on module.exportName, exactly like the object you’d get from import * as module.

A Realistic Use Case

A common pattern: load a module only in response to something, a button click, a specific route, a feature most users never touch.

javascript
const loadButton = document.getElementById("loadFeature");

loadButton.addEventListener("click", async () => {
  const { default: runHeavyFeature } = await import("./heavyFeature.js");
  console.log(runHeavyFeature());
});

Nothing inside heavyFeature.js downloads or runs until the button is actually clicked. For a small file this barely matters, for a large one, a charting library, a rich text editor, something most visitors to a page never touch, this can meaningfully speed up the initial page load.

Using .then() Instead of await

Since import() returns a promise, .then() works too, useful outside an async function.

javascript
import("./heavyFeature.js").then((module) => {
  console.log(module.default());
});
// Heavy feature ran

Destructuring the Result

Since the resolved value is just an object, you can destructure it directly, including renaming default the same way object destructuring always allows.

javascript
async function loadFeature() {
  const { default: run, VERSION } = await import("./heavyFeature.js");
  console.log(run(), VERSION);
}

loadFeature();
// Heavy feature ran 1.0

Try It

Imagine a file confetti.js:

javascript
// confetti.js
export default function celebrate() {
  return "🎉 Confetti!";
}

export const DURATION_MS = 2000;
  1. Write an async function that dynamically imports confetti.js, calls the default export, and logs the result.
  2. Rewrite the same thing using .then() instead of await.
  3. Given <button id="celebrateBtn">Celebrate</button>, add a click listener that dynamically imports confetti.js only when clicked, then calls its default export and logs the result.

Recap

  • A static import loads immediately and must sit at the top level of a file. Dynamic import() can go anywhere, runs when that line executes, and returns a promise.
  • import() is how you load a module only when it’s actually needed, useful for large or rarely used code.
  • The resolved value from import() is the same shape as import * as name, the default export on .default, named exports by their own names.

Next lesson: what a bundler does, and why almost every real project uses one alongside modules.