Dynamic Module Loading: import() Function
Dynamic imports let you defer code until it is actually needed. That makes them one of the most useful tools for reducing initial JavaScript cost, especially in large applications where many features are conditional, route-specific, or interaction-driven.
Quick Navigation: Basic Usage β’ How Dynamic Import Helps Code Splitting β’ Conditional Loading Patterns β’ Advanced Patterns and Caveats β’ Performance Trade-offs β’ Real-World Example β’ Best Practices
Quick Decision Guide
Dynamic import() loads a module at runtime and returns a Promise.
const module = await import('./module.js');Use it when code is not required immediately: route-level features, modals, admin panels, editors, heavy libraries, or user-triggered flows.
Main value: smaller initial bundle, less startup JavaScript, and better control over when code is fetched and executed.
Interview signal: import() is not just syntax sugar. It is a delivery strategy for moving non-critical code off the critical path.
Basic Usage
Syntax
const module = await import('./module.js');It returns a Promise that resolves to the module namespace object.
import('./module.js').then((module) => {
module.default();
});Destructuring Patterns
// Default export
const { default: Component } = await import('./Component.js');
// Named exports
const { func1, func2 } = await import('./utils.js');
// Entire namespace object
const module = await import('./module.js');
console.log(module.default);
console.log(module.namedExport);Error Handling
try {
const module = await import('./module.js');
} catch (error) {
console.error('Failed to load module:', error);
}Important Clarification
Static import is fixed at the top level and participates in the module graph up front.
Dynamic import() is evaluated at runtime, so it is useful when the decision to load code depends on user action, permissions, route, device, or feature availability.
How Dynamic Import Helps Code Splitting
Bundlers commonly use import() as a signal to create separate chunks that can be loaded on demand.
π₯ Insight
Code splitting is not the goal. The goal is to avoid forcing every user to download code for features they may never touch.
Example
if (user.isAdmin) {
const admin = await import('./admin-panel.js');
admin.default.init();
}Conceptually, this can become:
Why This Matters
Without splitting:
With splitting:
React Example
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
const Home = lazy(() => import('./pages/Home.js'));
const Dashboard = lazy(() => import('./pages/Dashboard.js'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
);
}Good Fit
Conditional Loading Patterns
Dynamic import is strongest when the code path is conditional.
Feature Flags
if (featureFlags.newEditor) {
const { RichEditor } = await import('./RichEditor.js');
return <RichEditor />;
}
const { SimpleEditor } = await import('./SimpleEditor.js');
return <SimpleEditor />;User Permissions
if (user.role === 'admin') {
const adminTools = await import('./admin-tools.js');
adminTools.initialize();
}User Interaction
async function openChartModal() {
const { default: ChartModal } = await import('./ChartModal.js');
showModal(ChartModal);
}Device or Environment
if (isMobile) {
const { default: MobileNav } = await import('./MobileNav.js');
return <MobileNav />;
}
const { default: DesktopNav } = await import('./DesktopNav.js');
return <DesktopNav />;Interview Takeaway
A strong use case is any feature where delaying code improves startup without hurting the common user path.
Advanced Patterns and Caveats
Parallel Loading
const [module1, module2, module3] = await Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js')
]);Use this when you know multiple modules are needed and do not want a waterfall.
Import Attributes
Dynamic import can also accept a second argument in environments that support import attributes.
const data = await import('./data.json', {
with: { type: 'json' }
});Dynamic Specifiers: Practical Limitation
const lang = 'en';
const translations = await import(`./locales/${lang}.json`);This can work when the bundler can analyze the possible files.
const path = getUserProvidedPath();
await import(path);This is much harder for bundlers to optimize because the target is fully dynamic.
Caching Behavior
const first = await import('./utils.js');
const second = await import('./utils.js');The module is generally loaded and evaluated once per environment, then reused from cache.
Common Mistake
Dynamic import does not mean the feature is free. If the user triggers it often or early, you are still paying download, parse, and execution cost β just later.
Performance Trade-offs
Benefits
Costs
π₯ Insight
The real question is not βCan I dynamically import this?β
It is:
> βIs this code rare enough, heavy enough, or late enough in the user journey that moving it out of startup is a net win?β
Good Candidates
Weak Candidates
Real-World Example
Before
import Chart from 'chart.js';
import RichEditor from 'quill';
import AdminPanel from './admin.js';Everything ships up front, even if the user never opens charts, never edits content, and is not an admin.
After
const loadChart = () => import('chart.js');
const loadEditor = () => import('quill');
const loadAdmin = () => import('./admin.js');Now those features move behind actual need.
Better Example with Interaction
async function exportReport(data) {
const { default: jsPDF } = await import('jspdf');
const pdf = new jsPDF();
pdf.text('Report', 10, 10);
pdf.save('report.pdf');
}This is a strong use case because PDF generation is heavy and not required during initial page load.
Best Practices
1. Split by user journey, not randomly
Move code out of startup only when it is not needed immediately.
2. Avoid waterfalls
If several dynamic modules are needed together, load them in parallel.
3. Show useful fallback UI
A loading state is often required when the user triggers code that is not yet downloaded.
4. Be careful with fully dynamic paths
Bundlers work best when the possible import targets are constrained and analyzable.
5. Do not oversplit
Too many tiny chunks can increase request overhead and complexity.
6. Measure actual impact
Track:
Final Rule of Thumb
Use dynamic import when a feature is optional, late, conditional, or heavy.
Do not use it for code that is required on the initial critical path.
Key Takeaways
import() loads modules at runtime and returns a Promiseimport() calls