Dynamic Imports — Loading JavaScript Modules at Runtime
In this tutorial, you will learn about Dynamic Imports. We cover key concepts, practical examples, and best practices to help you master this topic.
Dynamic imports use the import() function to load JavaScript modules on demand, enabling Code Splitting, Lazy Loading, and conditional feature loading in SPAs.
What You'll Learn
By the end of this tutorial, you will understand how to use dynamic import() syntax, how it differs from static imports, error handling patterns, and advanced use cases like conditional loading and preloading.
Why It Matters
Dynamic imports are the foundation of code splitting and lazy loading in modern SPAs. Without them, every JavaScript module is loaded upfront, bloating the initial bundle. Mastering dynamic imports gives you fine-grained control over when and how your application loads code.
Real-World Use
A multi-tenant SPA used dynamic imports to load tenant-specific modules. When a user from Tenant A logged in, only Tenant A's theming, components, and API clients loaded. Tenant B's code never downloaded. Initial bundle size dropped from 400KB to 180KB.
Dynamic Import Syntax
// Static import (loaded upfront)
import { formatDate } from './utils/date';
// Dynamic import (loaded on demand)
import('./utils/date').then(({ formatDate }) => {
const date = formatDate(new Date());
console.log(date);
});
// Dynamic import with async/await
async function loadDateUtils() {
const { formatDate } = await import('./utils/date');
return formatDate(new Date());
}
// Dynamic import with default export
async function loadTheme() {
const theme = await import('./themes/dark');
theme.apply(); // default export
}
// Dynamic import with multiple exports
async function loadDashboard() {
const module = await import('./features/dashboard');
module.init();
module.render(document.getElementById('dashboard'));
}
Dynamic Import Use Cases
// 1. Conditional feature loading
async function loadFeature(featureName) {
switch (featureName) {
case 'chart':
return import('./features/Chart');
case 'map':
return import('./features/Map');
case 'editor':
return import('./features/Editor');
case 'admin':
// Only admins need this
if (user.role !== 'admin') {
throw new Error('Unauthorized');
}
return import('./features/AdminPanel');
default:
throw new Error(`Unknown feature: ${featureName}`);
}
}
// 2. Polyfill loading (load only if needed)
async function ensurePolyfills() {
if (!window.IntersectionObserver) {
await import('intersection-observer');
console.log('IntersectionObserver polyfill loaded');
}
if (!window.fetch) {
await import('whatwg-fetch');
console.log('Fetch polyfill loaded');
}
}
// 3. Locale-based loading
async function loadLocale(locale) {
try {
const messages = await import(`./locales/${locale}.json`);
i18n.setMessages(messages);
} catch (error) {
console.warn(`Locale ${locale} not found, falling back to en`);
const messages = await import('./locales/en.json');
i18n.setMessages(messages);
}
}
Error Handling
async function loadFeatureSafely(featurePath) {
try {
const module = await import(featurePath);
return module;
} catch (error) {
if (error.code === 'MODULE_NOT_FOUND') {
console.error(`Feature ${featurePath} does not exist`);
return null;
}
if (error instanceof TypeError) {
console.error('Network error loading feature:', featurePath);
// Retry logic
return retryImport(featurePath, 3);
}
console.error('Unexpected error loading feature:', error);
return null;
}
}
async function retryImport(path, maxRetries) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await import(`${path}?retry=${attempt}`);
} catch (error) {
if (attempt === maxRetries) throw error;
await new Promise(r => setTimeout(r, 1000 * attempt));
}
}
}
Dynamic Import and Caching
// Module cache: dynamic imports are cached by the browser
// Subsequent imports of the same URL return the cached module
async function testCaching() {
const start1 = performance.now();
const mod1 = await import('./features/Chart');
const time1 = performance.now() - start1;
console.log(`First load: ${time1.toFixed(0)}ms`);
const start2 = performance.now();
const mod2 = await import('./features/Chart');
const time2 = performance.now() - start2;
console.log(`Second load (cached): ${time2.toFixed(0)}ms`);
}
// Preloading
async function preloadModule(path) {
const link = document.createElement('link');
link.rel = 'modulepreload';
link.href = path;
document.head.appendChild(link);
}
// Preload likely next features
preloadModule('./features/Dashboard');
preloadModule('./features/UserList');
Common Mistakes
- Using template literals with user input. Dynamic imports with user-constructed paths create security risks. Whitelist allowed paths instead.
- Forgetting to handle errors. A failed import (network error, 404) throws. Always wrap dynamic imports in try/catch.
- Importing the same module multiple times. While the module is cached, avoid redundant imports in the same function. Import once and store the reference.
- Not considering relative paths. Dynamic import paths are relative to the current file. Use absolute paths from the project root for clarity.
- Using dynamic imports for tiny modules. The overhead of a network request outweighs the benefit for modules under 1KB.
Practice Questions
- How does dynamic import() differ syntactically from static import?
- What happens if a dynamic import fails due to network error?
- How do you preload a module that will likely be needed soon?
- Are dynamic imports cached by the browser?
- Why should you avoid user input in dynamic import paths?
Challenge: Build a feature loader that: accepts a feature name, maps it to a safe module path, loads the module dynamically, handles errors with retry logic (3 attempts), caches the loaded module reference, and provides a preload function for likely-next features.
FAQ
Mini Project
Build a plugin-based SPA where features are loaded dynamically: define a plugin manifest with feature names and module paths, implement a FeatureLoader that loads plugins on demand with retry logic and caching, add a preloader for likely-next plugins, and handle all error cases gracefully.
What's Next
You know dynamic imports. Now optimize your bundle size with advanced optimization techniques.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro