ES Modules
Organize code with import/export syntax, dynamic imports, and module resolution in modern JavaScript runtimes.
Import and Export Syntax
ES modules use static `import` and `export` declarations analyzed at parse time, enabling tree shaking in bundlers. Named exports allow multiple bindings per file; default exports provide a single primary export per module.
Prefer named exports in application code — they improve refactoring support, auto-import accuracy, and avoid ambiguous default import names. Reserve default exports for framework entry points (pages, routes) where conventions require them.
- Named exports for most application modules
- Default exports for framework conventions
- Static imports enable tree shaking
export function formatDate(d) { /* ... */ }
export const API_URL = '/api';
import { formatDate, API_URL } from './utils.js';Module Scope and Strict Mode
Each module has its own top-level scope — variables do not become globals. Modules run in strict mode automatically, catching silent errors like assigning to undeclared variables.
Circular dependencies are allowed but dangerous: if module A imports B before B finishes initializing exports, A may receive live but uninitialized bindings. Restructure shared logic into a third module or use dependency injection to break cycles.
- Modules are always strict mode
- Top-level vars are module-scoped, not global
- Avoid circular dependencies when possible
Dynamic import()
`import()` returns a Promise resolving to the module namespace object. Use it for code splitting, lazy-loading routes, and loading optional features only when needed.
Dynamic imports work in async functions and enable conditional loading — for example, loading a heavy chart library only when the user opens the analytics tab. Bundlers create separate chunks automatically from dynamic import sites.
- Returns Promise of module namespace
- Enables route and feature code splitting
- Works conditionally at runtime
async function loadChart() {
const { renderChart } = await import('./chart.js');
renderChart(data);
}Package.json Module Fields
Node.js and bundlers resolve modules using `"type": "module"` for `.js` ESM files or `.mjs` extension. `"exports"` in package.json controls public entry points and prevents deep imports into internal paths.
Dual packages (CJS + ESM) remain tricky — prefer ESM-first for new libraries. Use `"import"` and `"require"` conditions in `exports` when both are required for compatibility.
- `"type": "module"` for ESM `.js` files
- `exports` field controls public API
- Prefer ESM-first for new packages
Production Module Patterns
Barrel files (`index.js` re-exporting everything) simplify imports but can hurt tree shaking and increase bundle size if they re-export large subgraphs. Prefer direct imports from source modules in performance-sensitive apps.
Use path aliases (`@/components`) via bundler/tsconfig configuration for readability without deep relative paths. Keep side effects explicit — modules that mutate global state on import should be documented and minimized.
- Avoid heavy barrel files in hot paths
- Path aliases for clean imports
- Minimize import-time side effects