Dynamic Imports
Objectives
By the end of this chapter, you should be able to:
- Explain the difference between a static
importand a dynamicimport() - Load a module on demand with
import() - Read a dynamically imported module’s default and named exports
💡 Why this matters: Every
importso 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.
// heavyFeature.js
export default function runHeavyFeature() {
return "Heavy feature ran";
}
export const VERSION = "1.0";
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.
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.
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.
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:
// confetti.js
export default function celebrate() {
return "🎉 Confetti!";
}
export const DURATION_MS = 2000;
- Write an
asyncfunction that dynamically importsconfetti.js, calls the default export, and logs the result. - Rewrite the same thing using
.then()instead ofawait. - Given
<button id="celebrateBtn">Celebrate</button>, add a click listener that dynamically importsconfetti.jsonly when clicked, then calls its default export and logs the result.
Recap
- A static
importloads immediately and must sit at the top level of a file. Dynamicimport()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 asimport * 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.