Mental model
ESM (import/export) evaluates static module dependency graphs before code execution, whereas CommonJS (require()) loads modules dynamically at runtime.
Theory
Because ESM imports must appear at top-level, modern bundlers (Vite, Rollup, Turbopack, esbuild) construct a static Abstract Syntax Tree (AST) graph of all exported symbols. Unreferenced exports are eliminated during dead code elimination (Tree-Shaking). In TypeScript 5.0+, setting moduleResolution: "bundler" aligns type resolution with modern web build tools.
Alternatives and trade-offs
- CommonJS (
require): Supports dynamic path evaluation (require('./' + name)), but prevents static analysis and dead code elimination. - ESM (
import): Static, tree-shakeable, and natively supported across modern browsers and Node.js 18+.
Failure modes and misconceptions
- Side-effect imports: Importing files with top-level side effects (e.g.
import './polyfills') prevents bundlers from shaking those modules. Mark"sideEffects": falseinpackage.json. - CJS/ESM dual package hazard: Mixing CJS
requireand ESMimportfor the same library can load two separate singleton instances into memory.
Decision scenario
Configure TypeScript applications targeting modern bundlers (Vite, Next.js) with "moduleResolution": "bundler" and "sideEffects": false in library packages.
Learning outcomes
- Contrast static ESM module graphs with dynamic CommonJS require paths.
- Configure
moduleResolution: "bundler"for modern frontend tooling. - Optimize export structures to enable tree-shaking dead code elimination.
Trade-offs
ESM enables aggressive tree-shaking and smaller production bundle sizes, but requires consistent package exports configuration across full-stack repositories.