JS: Module. Dynamic Import
(new in ECMAScript 2020)
What is Dynamic Import
Dynamic import is introduced in ECMAScript 2020. It let you import a module at run-time. You can import a module by condition inside a if-statement, and module name can also be a variable.
Import and export of ECMAScript 2015 are static. That means, the code runs at compile time. You cannot import at run time, such as import a lib only when needed.
Dynamic Import Syntax
import(moduleSpecifier)
read
import(moduleSpecifier).then((x_module) => { // x_module is a object. keys are exported names. const { default: MyComponent } = x_module; });
Full Example
// file name: lib.js const f1 = ((x) => (x + 1)); const f2 = ((x) => (x + 2)); export { f1, f2 };
// Static import (for comparison) - must be at top level // import { greet } from './utils.js'; // This would fail if conditional async function loadAndUseUtils() { try { // Dynamic import with a variable path (e.g., based on condition) const modulePath = './utils.js'; // Could be dynamic: `./modules/${feature}.js` const utils = await import(modulePath); // Await the Promise // Access named export console.log(utils.greet('World')); // "Hello, World!" // Access default export const FancyGreet = utils.default; console.log(FancyGreet('Grok')); // "🌟 Hello, Grok! 🌟" } catch (error) { console.error('Failed to load module:', error); } } // Trigger on button click (lazy loading example) document.getElementById('load-btn').addEventListener('click', loadAndUseUtils);
In
ECMAScript 2022
, you can use await at the top level in modules (not scripts), making dynamic imports cleaner.
const utils = await import('./utils.js'); console.log(utils.greet('Top-level!'));
ai answer
JavaScript Dynamic Imports: A Complete Tutorial
Dynamic imports (also known as import() expressions) allow you to load JavaScript modules on demand at runtime, rather than statically at the top of your file. This is one of the most powerful features for performance optimization in modern JavaScript.
Why Use Dynamic Imports?
- Code Splitting: Load only the code the user needs (e.g., a heavy chart library only when the user opens the analytics tab).
- Lazy Loading: Improve initial page load time.
- Conditional Loading: Import modules based on user actions, feature flags, or environment.
- Better Bundle Sizes: Modern bundlers (Webpack, Rollup, Vite, esbuild, etc.) automatically create separate chunks for dynamic imports.
- Tree Shaking still works effectively.
Static import (eager):
import HeavyLibrary from './heavy-library.js'; // Loaded immediately
Dynamic import (lazy):
const button = document.getElementById('load-chart'); button.addEventListener('click', async () => { const { default: HeavyLibrary } = await import('./heavy-library.js'); // Now use it });
Basic Syntax
The import() function returns a Promise that resolves to the module namespace object.
import(moduleSpecifier) .then(module => { // module.default, module.namedExport, etc. }) .catch(err => console.error(err));
With async/await (recommended):
async function loadModule() { try { const module = await import('./my-module.js'); console.log(module); } catch (err) { console.error('Failed to load module:', err); } }
Module Specifier
The argument to import() can be:
-
A static string (most common):
await import('./utils.js'); await import('/absolute/path/module.js'); await import('https://cdn.example.com/lib.js'); -
A variable/dynamic string:
async function loadLanguage(lang) { const module = await import(`./languages/${lang}.js`); return module.translations; }
Note: You cannot use template literals with variables in some bundlers unless configured properly (most modern bundlers support it).
Working with Exports
Default Export
// math.js export default function add(a, b) { return a + b; }
// main.js const { default: add } = await import('./math.js'); // or const math = await import('./math.js'); const result = math.default(2, 3);
Named Exports
// utils.js export const PI = 3.14; export function square(x) { return x * x; }
const utils = await import('./utils.js'); console.log(utils.PI); console.log(utils.square(5));
You can also destructure directly:
const { PI, square } = await import('./utils.js');
Real-World Examples
1. Lazy Loading a Component (React example)
async function loadComponent() {
const { default: HeavyComponent } = await import('./HeavyComponent.jsx');
// Render it
}
Many frameworks (Next.js, React.lazy, Vue, Svelte) have built-in support that uses dynamic imports under the hood.
2. Feature Flags
async function initializeApp() { if (userHasPremium) { const { PremiumFeatures } = await import('./premium.js'); PremiumFeatures.init(); } }
3. Route-based Code Splitting
const routes = { '/dashboard': () => import('./pages/Dashboard.js'), '/settings': () => import('./pages/Settings.js'), }; async function navigate(path) { const module = await routes[path](); // render module.default or similar }
Error Handling
Always wrap dynamic imports in try/catch:
async function safeImport(path) { try { return await import(path); } catch (error) { if (error.name === 'TypeError') { console.error('Module loading failed - network or syntax error'); } else { console.error('Unexpected error:', error); } // Fallback UI or retry logic } }
Advanced Topics
Importing JSON (modern browsers)
const data = await import('./config.json', { assert: { type: 'json' } }); // or in newer syntax: const data = await import('./config.json', { with: { type: 'json' } });
Dynamic Import in CommonJS (require)
Dynamic imports work in both ESM and CommonJS modules.
Caching
Modules loaded with import() are cached just like static imports. Subsequent calls to the same specifier return the cached module instantly.
const m1 = await import('./foo.js'); const m2 = await import('./foo.js'); // Returns cached module
To force a reload (rarely needed), use cache-busting query params:
await import(`./foo.js?v=${Date.now()}`);
Browser and Environment Support
- Modern browsers: Fully supported (Chrome 63+, Firefox 67+, Safari 11.1+).
- Node.js: Supported since v13.2.0 (with
--experimental-modulesearlier). - Bundlers: Excellent support in Vite, Webpack 4+, Parcel, Rollup, etc.
Polyfill for very old browsers (not recommended for new projects):
<script nomodule src="dynamic-import-polyfill.js"></script>
Best Practices
- Use async/await for readability.
- Keep the import call close to where the module is used.
- Avoid putting dynamic imports in the top level of modules unless necessary (can delay module evaluation).
- Preload critical chunks using
<link rel="modulepreload">for better performance. - Handle loading states (spinners, skeletons) in UI.
- Monitor bundle size — dynamic imports create new network requests.
Common Pitfalls
- Top-level await in modules can sometimes be combined with dynamic imports.
- Dynamic imports do not hoist (unlike static
import). - Relative paths behave differently depending on the environment.
- In some bundlers, dynamic variables may require special configuration (
webpackChunkNamecomments, etc.).
Comparison: Static vs Dynamic
| Feature | Static import |
Dynamic import() |
|---|---|---|
| Loading time | Build time / initial | Runtime (on demand) |
| Bundle splitting | Limited | Excellent |
| Tree shaking | Full | Full |
| Conditional loading | Not possible | Perfect |
| Top-level syntax | Yes | Function call |
Exercise: Create a simple demo with a button that loads a different theme module (light-theme.js / dark-theme.js) dynamically and applies it to the page.
Would you like me to expand any section, provide a full working demo project structure, or show integration with a specific framework (React, Vue, Svelte, Node.js)?