Explore advanced ES Modules features including dynamic imports, top-level await, live bindings, and handling circular dependencies.
Use the import() function to lazy-load modules on demand for code splitting and improved performance.
// Load module dynamically
const module = await import('./heavy-module.js');
module.doSomething();
// Conditional loading
if (userIsAdmin) {
const adminFeature = await import('./admin-panel.js');
adminFeature.init();
}
// Dynamic path (use with caution)
const lang = 'en';
const translations = await import(`./i18n/${lang}.js`);
Benefits:
Use await outside of an async function at the top level of a module.
// Fetch config before app loads
const config = await fetch('/api/config').then(r => r.json());
// Load database connection
import { connectDB } from './database.js';
const db = await connectDB();
export { config, db };
⚠️ Important:
Top-level await blocks module loading. Modules that import this module will wait until the await completes.
Imports are live, read-only views into exported variables. Changes in the exporting module are reflected in importing modules.
// counter.js
export let count = 0;
export function increment() { count++; }
// main.js
import { count, increment } from './counter.js';
console.log(count); // 0
increment();
console.log(count); // 1 (live binding!)
// count = 10; // ❌ Error: Assignment to constant variable
// Imports are read-only!
ES Modules' live bindings can handle some circular dependency cases gracefully, unlike CommonJS.
// a.js
import { b } from './b.js';
export const a = 'a';
console.log('a.js:', b); // Works!
// b.js
import { a } from './a.js';
export const b = 'b';
console.log('b.js:', a); // May be undefined during init
// main.js
import './a.js'; // Resolves circular dependency
✅ Best Practice:
Avoid circular dependencies when possible. Refactor shared code into a third module.
import.meta provides metadata about the current module.
// Get module URL
console.log(import.meta.url);
// file:///path/to/module.js or https://example.com/module.js
// Load assets relative to module
const imagePath = new URL('./image.png', import.meta.url);
// Resolve a module specifier to a URL (all major browsers + Node 20.6+)
const resolvedUrl = import.meta.resolve('./utils.js');
console.log(resolvedUrl); // https://example.com/utils.js
// Node.js file path helpers (Node 21.2+ / 20.11+)
console.log(import.meta.dirname); // /home/user/project/src
console.log(import.meta.filename); // /home/user/project/src/app.js
// Replaces __dirname and __filename from CommonJS
// In Vite/Webpack - environment variables
console.log(import.meta.env.VITE_API_KEY);
See advanced ES Module features in action - dynamic imports and top-level await executing live. Click "Edit in CodePen" to experiment!
Import attributes allow you to pass metadata to the module loader using the with keyword. Standardized in ECMAScript 2025, they replace the older assert syntax.
// JSON modules - parse JSON as a module
import config from './config.json' with { type: 'json' };
console.log(config.apiUrl);
// CSS module scripts (Chrome/Edge)
import styles from './styles.css' with { type: 'css' };
document.adoptedStyleSheets = [styles];
// Dynamic import with attributes
const data = await import('./data.json', {
with: { type: 'json' }
});
Migration Note:
If your code uses the older assert keyword, update to with. The assert syntax has been removed from the specification.
The import defer proposal (TC39 Stage 3) allows importing a module without immediately executing it. The module evaluates lazily when you first access one of its exports.
// Module is fetched and linked, but NOT executed yet
import defer * as analytics from './heavy-analytics.js';
// Later, when actually needed...
function trackEvent(event) {
// NOW the module executes synchronously on first access
analytics.track(event);
}
// Useful for: startup performance, conditional features,
// rarely-used code paths
Tooling Support (2026):
TypeScript 5.9+, Babel, webpack, and Prettier already support import defer syntax. Browser implementations in V8 and JavaScriptCore are underway.
Source phase imports (TC39 Stage 3) provide access to a compiled module source without evaluating it. The initial focus is WebAssembly module integration.
// Import a WebAssembly module source
import source wasmModule from './processor.wasm';
// Instantiate with custom imports
const instance = await WebAssembly.instantiate(
wasmModule,
{ env: { memory: new WebAssembly.Memory({ initial: 1 }) } }
);
instance.exports.process(data);
Runtime Support:
Chrome V8 (M131+), Deno 2.6+. Node.js implementation is in progress. This enables native WebAssembly ESM integration without manual fetch/compile steps.