Dynamic Import
This lesson explains Dynamic Import with clear examples, use cases, and best practices so you can use modern JavaScript confidently in real projects.
ES6 Dynamic Imports Overview
Dynamic imports allow JavaScript modules to be loaded
only when they are needed. Instead of loading every module during the
initial page load, dynamic imports use the import() function
to load code asynchronously at runtime.
Dynamic imports are commonly used for code splitting, lazy loading,
route-based loading, modal loading, chart libraries, admin features,
language files, heavy utilities, feature flags, and performance
optimization in modern JavaScript applications.
| Feature | Description |
| Syntax | import("./module.js") |
| Return Value | Returns a Promise. |
| Loading Time | Runtime, when code is needed. |
| Main Benefit | Improves initial load performance through lazy loading. |
| Works With | Default exports, named exports, async await, bundlers, routes. |
| Common Tools | Vite, Webpack, Rollup, Parcel, Next.js, Astro. |
Static Import vs Dynamic Import
// Static import
// Loaded before the module runs
import {
formatPrice
}
from "./formatters.js";
console.log(
formatPrice(100)
);
// Dynamic import
// Loaded only when this code runs
import("./formatters.js")
.then((module) => {
console.log(
module.formatPrice(100)
);
});
Static imports are loaded upfront. Dynamic imports are loaded later when
the application actually needs them.
Basic Dynamic Import Example
async function loadFormatter() {
const module =
await import("./formatters.js");
const result =
module.formatPrice(99.99);
console.log(result);
}
loadFormatter();
Because import() returns a Promise, it works naturally with
async and await.
Dynamic Import with Named Exports
// math.js
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
// app.js
async function calculate() {
const math =
await import("./math.js");
console.log(
math.add(10, 20)
);
console.log(
math.multiply(5, 4)
);
}
calculate();
Named exports are accessed as properties on the imported module object.
Dynamic Import with Default Export
// User.js
export default class User {
constructor(name) {
this.name =
name;
}
}
// app.js
async function loadUser() {
const module =
await import("./User.js");
const User =
module.default;
const user =
new User("Alice");
console.log(user);
}
loadUser();
Default exports are available through module.default.
Destructuring Dynamic Imports
async function loadHelpers() {
const
{
formatDate,
formatCurrency
}
=
await import("./helpers.js");
console.log(
formatDate(new Date())
);
console.log(
formatCurrency(1200)
);
}
Destructuring helps directly extract named exports from the loaded module.
Lazy Load Modal Logic
async function openSettingsModal() {
const
{
SettingsModal
}
=
await import("./SettingsModal.js");
const modal =
new SettingsModal("#settings");
modal.open();
}
document
.querySelector("#settingsButton")
.addEventListener(
"click",
openSettingsModal
);
Modal logic can be loaded only when the modal is opened for the first
time.
Route-Based Dynamic Imports
const routes =
{
"/":
() => import("./pages/HomePage.js"),
"/products":
() => import("./pages/ProductsPage.js"),
"/admin":
() => import("./pages/AdminPage.js")
};
async function loadRoute(path) {
const loadPage =
routes[path];
if (!loadPage) {
return;
}
const pageModule =
await loadPage();
pageModule.render();
}
Route-based dynamic imports are common in single-page applications to
split each page into a separate JavaScript chunk.
Conditional Dynamic Import
async function loadAdminTools(user) {
if (user.role !== "admin") {
return;
}
const adminTools =
await import("./admin-tools.js");
adminTools.initialize();
}
Conditional imports help load features only for users who need them.
Feature-Based Dynamic Import
async function enableFeature() {
if ("IntersectionObserver" in window) {
const lazy =
await import("./lazy-load.js");
lazy.enableLazyLoading();
} else {
const fallback =
await import("./lazy-load-fallback.js");
fallback.enableFallback();
}
}
Dynamic imports work well with feature detection and fallback modules.
Load Language Files Dynamically
async function loadLanguage(language) {
const messages =
await import(
"./languages/" + language + ".js"
);
return messages.default;
}
loadLanguage("en")
.then((messages) => {
console.log(messages);
});
Internationalized applications often load only the current user's language
file.
Load Heavy Library Only When Needed
async function exportPdf() {
const pdf =
await import("./pdf-export.js");
pdf.generatePdf();
}
document
.querySelector("#exportPdf")
.addEventListener(
"click",
exportPdf
);
Heavy features such as PDF export, charts, maps, editors, and analytics
tools should often be loaded on demand.
Error Handling with Dynamic Imports
async function loadFeature() {
try {
const feature =
await import("./feature.js");
feature.start();
} catch (error) {
console.error(
"Failed to load feature:",
error.message
);
}
}
Dynamic imports can fail if the file path is wrong, the network is down,
or the generated chunk cannot be loaded.
Show Loading State
async function openReport() {
const button =
document.querySelector("#reportButton");
button.disabled =
true;
button.textContent =
"Loading...";
try {
const report =
await import("./report.js");
report.render();
} finally {
button.disabled =
false;
button.textContent =
"Open Report";
}
}
Show loading feedback when dynamically loaded modules may take time.
Preload Module on Hover
let reportModulePromise =
null;
function preloadReport() {
if (!reportModulePromise) {
reportModulePromise =
import("./report.js");
}
}
async function openReport() {
const report =
await reportModulePromise;
report.render();
}
document
.querySelector("#reportButton")
.addEventListener(
"mouseenter",
preloadReport
);
document
.querySelector("#reportButton")
.addEventListener(
"click",
openReport
);
Preloading on hover improves perceived performance by starting the module
download before the click happens.
Cache Dynamic Import Result
let chartModule =
null;
async function getChartModule() {
if (!chartModule) {
chartModule =
await import("./chart.js");
}
return chartModule;
}
async function renderChart() {
const chart =
await getChartModule();
chart.renderChart();
}
Browsers and bundlers usually cache modules, but storing the loaded module
reference can make application logic easier to control.
Vite import.meta.glob Example
const pages =
import.meta.glob(
"./pages/*.js"
);
async function loadPage(name) {
const pagePath =
"./pages/" + name + ".js";
const load =
pages[pagePath];
if (!load) {
return;
}
const page =
await load();
page.render();
}
import.meta.glob is commonly used in Vite projects for
dynamic route or file-based module loading.
Dynamic Import vs Static Import
| Feature | Static Import | Dynamic Import |
| Syntax | import x from "./x.js" | import("./x.js") |
| Loading | Before module execution. | At runtime when called. |
| Returns | Imported binding. | Promise resolving to module object. |
| Best Use | Core application code. | Lazy loaded or conditional code. |
| Can Be Conditional? | No. | Yes. |
| Code Splitting | Usually bundled upfront. | Creates separate chunks in bundlers. |
Common Dynamic Import Use Cases
- Lazy loading pages.
- Route-based code splitting.
- Loading admin-only features.
- Loading modals on demand.
- Loading chart libraries.
- Loading map libraries.
- Loading language files.
- Loading PDF or export tools.
- Feature flag based loading.
- Browser feature fallback loading.
Common Dynamic Import Mistakes
- Forgetting that
import() returns a Promise. - Trying to use dynamically imported modules synchronously.
- Forgetting
module.default for default exports. - Using fully dynamic paths that bundlers cannot analyze.
- Not handling loading errors.
- Not showing loading states for slow chunks.
- Lazy loading tiny modules unnecessarily.
- Creating too many small chunks.
- Loading critical code too late.
- Ignoring browser or network failures.
Dynamic Import Best Practices
- Use dynamic imports for heavy or optional features.
- Keep core application code statically imported.
- Handle errors with
try...catch. - Show loading states when imports are user-triggered.
- Cache loaded modules when reused frequently.
- Use route-level code splitting for large applications.
- Preload important modules on hover or idle time.
- Avoid over-splitting into too many tiny chunks.
- Use bundler-supported patterns for dynamic file paths.
- Test production builds because bundler behavior matters.
Dynamic Import Interview Questions
- What is a dynamic import?
- How is
import() different from static import? - What does
import() return? - How do you access a default export from a dynamic import?
- How do you access named exports from a dynamic import?
- How does dynamic import help performance?
- What is code splitting?
- When should dynamic imports be avoided?
- How do bundlers handle dynamic imports?
- What are common dynamic import mistakes?
Key Takeaways
import() loads JavaScript modules at runtime. - Dynamic imports return a Promise.
- Use
await import("./module.js") inside async functions. - Default exports are accessed with
module.default. - Named exports are accessed from the module object or destructuring.
- Dynamic imports improve performance by loading code only when needed.
- Bundlers use dynamic imports for code splitting.
- Always handle loading errors and slow network cases.
Pro Tip
Use dynamic imports for code that users may not need immediately, such as
admin tools, reports, charts, modals, maps, and export features. Keep
critical above-the-fold logic in static imports so the application starts
quickly and predictably.