JS Modules — ES Modules
Master native modular architecture: named vs default export, static vs dynamic import(), browser module execution semantics (type="module"), and modern Import Maps.
Static ESM Resolution vs Dynamic Runtime Loading
ES Modules are statically analyzed at compile/parse time. Top-level imports construct an asynchronous dependency graph before any code runs, while dynamic import() enables lazy on-demand code splitting.
1. Named vs Default Exports & Re-exporting
ES Modules allow two fundamental export flavors. Named exports are statically verified by linters and bundlers, while default exports represent the primary entity of a module file.
// utils.js
export const API_URL = 'https://api.site.com';
export function calculateTax(amount) {
return amount * 0.15;
}
// consumer.js
import { API_URL, calculateTax as getTax } from './utils.js';
// UserProfile.js
export default class UserProfile {
constructor(name) { this.name = name; }
}
// consumer.js (Can import using any arbitrary name!)
import ProfileComponent from './UserProfile.js';
The Barrel Re-Export Pattern
Group multiple submodule exports into a single clean entrypoint index:
// components/index.js
export { Button } from './Button.js';
export { Modal } from './Modal.js';
export { Card } from './Card.js';
// Application consumption:
import { Button, Modal, Card } from './components/index.js';
Test in Playground
2. Browser Module Execution: type="module"
When you add <script type="module" src="app.js"></script> to your HTML, the browser fundamentally changes its script execution behavior:
Module scripts defer automatically: HTML parsing continues uninterrupted, and execution occurs only when the DOM is fully constructed.
"use strict" is on by default. Top-level variables remain module-scoped and never pollute the global window object.
Loaded via CORS (fails on file://). Each module executes exactly once, even if imported by 10 different files.
Modern Import Maps (No Bundler Required)
Import Maps let native browsers resolve "bare specifiers" (like import lodash from 'lodash') directly without Vite or Webpack:
<script type="importmap">
{
"imports": {
"lodash": "https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/lodash.js",
"chart": "/js/components/chart.esm.js"
}
}
</script>
<script type="module">
import _ from 'lodash'; // Resolves via importmap!
console.log(_.capitalize('aicodelab'));
</script>
Run in Playground
3. Dynamic import() & Route-Based Code Splitting
Static import ... from ... must appear at the top-level of a module. For route-based lazy loading or conditional features, dynamic import('path') can be called anywhere inside functions and returns a Promise resolving to the module's namespace object.
On-Demand Feature Loading
button.addEventListener('click', async () => {
// Only downloads heavy PDF library when user clicks export!
try {
const { generatePdfReport } = await import('./heavyPdfGenerator.js');
generatePdfReport(reportData);
} catch (err) {
console.error('Failed to load module:', err);
}
});
Test in Playground
Build an Extensible Plugin Registry with Dynamic Loading
Implement a PluginRegistry class that accepts a plugin URL, dynamically loads it via import(), ensures it implements required lifecycle hooks (name and init()), and executes initialization.
class PluginRegistry {
constructor() {
this.plugins = new Map();
}
async loadPlugin(moduleUrl) {
try {
const module = await import(moduleUrl);
const plugin = module.default || module;
if (!plugin.name || typeof plugin.init !== 'function') {
throw new Error(`Plugin at ${moduleUrl} does not conform to specification`);
}
await plugin.init();
this.plugins.set(plugin.name, plugin);
console.log(`Plugin '${plugin.name}' initialized successfully.`);
return plugin;
} catch (err) {
console.error(`Error loading plugin from ${moduleUrl}:`, err.message);
throw err;
}
}
}
Chapter 33 Knowledge Check
Validate your mastery of ES Modules, browser execution semantics, and dynamic loading.
window object?<script> tag to execute it as an ES Module?import('path.js') return?<script type="importmap">?