Dynamic Imports in JavaScript: Complete Guide
Dynamic imports let you load JavaScript modules on demand instead of at page load. Learn the import() syntax, lazy loading patterns, and how to split code for faster websites.
Static imports load every module at parse time, before any code runs. JavaScript dynamic imports flip that: they load a module only when your code asks for it, returning a Promise that resolves when the module is ready.
Here is the difference in one example:
// Static import - loads at parse time, always
import { heavyFunction } from "./heavy-module.js";
heavyFunction();
// Dynamic import - loads only when this line executes
const { heavyFunction } = await import("./heavy-module.js");
heavyFunction();The static version loads heavy-module.js immediately, even if the user never triggers this code path. The dynamic version loads it only when execution reaches the import() call. For large libraries, charts, or admin-only features, this can cut initial page load time significantly.
The import() Function Syntax
import() looks like a function call but is actually a special syntax called an import expression. It takes a module specifier string and returns a Promise:
import("./module.js").then((module) => {
// module is the namespace object
console.log(module);
});The resolved module object contains all the module's exports. Here is a source module with named exports and a default export to demonstrate:
// math.js
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
export default function multiply(a, b) { return a * b; }When you dynamically import it, access named exports as properties and the default export through the .default property on the resolved module object:
// app.js
const math = await import("./math.js");
console.log(math.add(2, 3)); // 5 (named export)
console.log(math.subtract(10, 4)); // 6 (named export)
console.log(math.default(3, 4)); // 12 (default export)The browser fetches the module file over the network, parses it, executes its top-level code once, and returns the exports. If the fetch fails or the module throws during execution, the Promise rejects.
Dynamic Imports with async/await
The cleanest way to use dynamic imports is with async/await syntax. This pattern makes the code read top-to-bottom instead of chaining .then() calls.
The approach is especially important for loading heavy libraries like charting tools, which should never block the initial page load. Here is a wrapper function that safely loads Chart.js and returns it, or null if the load fails:
async function loadChartLibrary() {
try {
const { Chart, registerables } = await import("chart.js");
Chart.register(...registerables);
return Chart;
} catch (error) {
console.error("Failed to load chart library:", error);
return null;
}
}Now call this function only when the user triggers the feature. The library downloads on click, not on page load:
button.addEventListener("click", async () => {
const Chart = await loadChartLibrary();
if (Chart) {
new Chart(canvas, config);
}
});The chart library only downloads when the user clicks the button. Most users may never click it, so you do not force everyone to download Chart.js upfront.
Destructuring Named Exports from Dynamic Imports
You can destructure named exports directly from the awaited result object. This works the same as destructuring any other JavaScript object:
const { format, parseISO, differenceInDays } = await import("date-fns");
console.log(format(new Date(), "yyyy-MM-dd"));This is clean and readable. Just make sure the names match what the module actually exports.
Conditional Module Loading
Dynamic imports shine when you need different code based on runtime conditions. The function below picks the right language module based on the user's locale:
async function getTranslationModule(userLocale) {
if (userLocale === "es") {
return import("./translations/es.js");
}
if (userLocale === "fr") {
return import("./translations/fr.js");
}
return import("./translations/en.js");
}Calling the function loads only the matching translation module. Here is how you use it with a Spanish locale setting:
const translations = await getTranslationModule("es");
console.log(translations.greeting); // HolaInstead of bundling every language into the main bundle, only the user's language loads. This is how production i18n systems keep bundle sizes small.
Loading on User Interaction
A practical pattern: load heavy functionality only when the user signals intent by interacting with a specific element. The search feature is a classic use case.
Most visitors never use search, so bundling a search engine library upfront wastes bandwidth for everyone. The approach below delays the import until the user focuses the search input:
const searchInput = document.querySelector("#search");
searchInput.addEventListener("focus", async () => {
const { createSearchEngine } = await import("./search-engine.js");
const engine = createSearchEngine(searchInput);
engine.attach();
}, { once: true });The { once: true } option ensures the import only happens the first time the user focuses the search input. On subsequent focuses, the module is already cached by the browser.
Error Handling for Dynamic Imports
Network failures, 404 errors, and module execution errors all reject the Promise returned by import(). Always handle these failures rather than letting them crash your application.
A robust pattern wraps the import in a safety function that returns null on failure. Here is the wrapper:
async function safeImport(path) {
try {
return await import(path);
} catch (error) {
console.warn(`Could not load ${path}:`, error.message);
return null;
}
}Now call the wrapper and handle the null case gracefully so the rest of the app can continue without crashing:
const module = await safeImport("./optional-feature.js");
if (module) {
module.init();
} else {
console.log("Feature unavailable, continuing without it.");
}Without error handling, a single failed dynamic import can crash your entire application.
Common Mistake: Forgetting That import() Returns a Promise
Dynamic imports are asynchronous. This code will not work:
// Wrong - module is a Promise, not the module object
const module = import("./math.js");
console.log(module.add(2, 3)); // TypeError: module.add is not a function
// Correct - await the Promise
const module = await import("./math.js");
console.log(module.add(2, 3)); // 5The import() expression always returns a Promise. You must await it or use .then().
Dynamic Imports and Bundlers
Modern bundlers like Vite and Webpack automatically detect import() calls and split them into separate files (chunks). When you write:
const module = await import("./heavy-module.js");The bundler extracts heavy-module.js and its dependencies into a separate chunk. The main bundle does not include it. The chunk only downloads when the import() call executes at runtime.
This automatic splitting is the foundation of route-level code splitting and advanced code splitting strategies. You write normal dynamic imports, and the bundler handles the rest.
Rune AI
Key Insights
import()returns a Promise that resolves to a module namespace object.- Use dynamic imports to load code only when needed, reducing initial bundle size.
- Always wrap dynamic imports in try/catch to handle loading failures.
- Destructure named exports from the resolved module object.
- Dynamic imports enable route-level code splitting in frameworks and SPAs.
Frequently Asked Questions
What is the difference between static and dynamic imports?
Can I use dynamic imports with named exports?
Do dynamic imports work in all browsers?
Conclusion
Dynamic imports are the foundation of modern code splitting. By loading modules only when they are actually needed, you reduce the initial JavaScript bundle size and improve page load speed. Use them for large libraries, route-level components, and features that most users never access. Combine dynamic imports with bundler code splitting to build fast, scalable web applications.
More in this topic
Using Decorators for Logging in JS Architecture
Learn how JavaScript decorators wrap class methods to add logging without touching the original code, including setup with Babel and how execution order works.
JavaScript While Loop Explained: A Complete Guide
A while loop repeats code as long as a condition stays true. Learn the syntax, see practical examples, and understand when a while loop is the right choice over a for loop.
JavaScript Loops Tutorial: for, while, do while
Loops let JavaScript repeat code without writing it twice. Learn the three core loop types, what each one does, and when to choose one over another.