ES Modules
export/import, dynamic imports, tree-shaking, CJS vs ESM
What you'll learn
Use named and default exports/imports
Dynamically import with import()
Explain tree-shaking
Compare CommonJS vs ESM
Named Exports
// math.js
export const PI = 3.14159
export function add(a, b) {
return a + b
}
// app.js
import { PI, add } from "./math.js"
import * as math from "./math.js"
import { add as sum } from "./math.js"
Default Exports
One per module. No name required at import.
// logger.js
export default function log(msg) {
console.log("[LOG]", msg)
}
// app.js
import log from "./logger.js" // any name works
import User, { ROLES } from "./user.js" // mix default + named
Dynamic import()
Load on demand — code splitting.
// Static: bundled eagerly
import { format } from "date-fns"
// Dynamic: code-split
if (isAdmin) {
const admin = await import("./admin-panel.js")
admin.init()
}
Code Splitting
Dynamic import() creates separate chunks loaded only when needed.
Module Resolution
import { useEffect } from "react" // bare specifier → node_modules
import { helper } from "./utils.js" // relative — extension required
Tree-Shaking
Bundlers remove unused exports traced from static imports.
// ✅ Named exports: tree-shakeable
export function used() {}
export function unused() {} // eliminated
// ❌ export default { a, b } — hard to tree-shake
Anti-Pattern
export * from "./utils" (barrel files) kills tree-shaking. Direct imports
are safer.
CommonJS vs ESM
| Feature | CJS | ESM |
|---|---|---|
| Syntax | require/exports | import/export |
| Loading | Sync, runtime | Async, parse-time |
| Tree-shaking | No | Yes |
| Top-level await | No | Yes |
See It In Action
Refactor into Modules
Code
typescript
1// ---- lib/api.ts (module) ----2export const API_URL = "https://api.example.com"3 4export function handleError(e: unknown) {5console.error(e instanceof Error ? e.message : String(e))6}7 8export async function fetchUser(id: string) {9try {10 const r = await fetch(API_URL + '/users/' + id)11 if (!r.ok) throw Error('HTTP ' + r.status)12 return await r.json()13} catch (e) {14 handleError(e)15 return null16}17}18 19// ---- app/user.ts (importing module) ----20import { fetchUser } from "./lib/api"21 22export async function renderUser(id: string) {23const u = await fetchUser(id)24if (!u) return '<p>User not found</p>'25return '<h1>' + u.name + '</h1>'26}Modules separate concerns: `lib/api.ts` owns data fetching, `app/user.ts` owns rendering. Named exports (`export`) and imports (`import { … }`) make dependencies explicit and enable tree-shaking.
Lesson Summary
Key Takeaways
Named exports for tree-shaking; one default export per module
Dynamic import() for code splitting and lazy loading
Tree-shaking removes unused exports via static analysis
CJS: require (sync, runtime). ESM: import (async, static)
Barrel files kill tree-shaking — prefer direct imports