ESM là standard với static imports và tree shaking — CJS dynamic require() không tree-shakeable; browser native support ESM; Vite dev server nhanh vì không bundle, serve native ESM; CJS không thể require() ESM (async).
CommonJS (CJS): require() synchronous, dynamic (có thể dùng trong if/function), module.exports là bất kỳ giá trị. Node.js default. Không tree-shakeable (bundler không biết exports nào được dùng vì dynamic). ESM: import/export static (phải ở top-level), asynchronous loading, chính thức là JavaScript standard (TC39). Tree-shakeable. Top-level await. import.meta.url. Tại sao ESM thắng: Tree shaking — ESM static structure cho phép bundler eliminate dead code (cùng import 1 hàm từ lodash-es vs lodash cũ); Async loading — ESM modules load async, CJS synchronous blocking; Standard — browsers native support ESM, Node.js thêm support từ v12; Better DX — Vite native ESM dev server cực nhanh vì không bundle. Node.js interop hiện tại: CJS có thể require() CJS; ESM có thể import CJS (chỉ default export); CJS không thể require() ESM (async) — phải dùng dynamic import(). Dual package (publish cả CJS lẫn ESM): exports field trong package.json với conditional exports. Pitfall: Dual package hazard — app có thể load cả CJS lẫn ESM version của cùng package (2 instances, state không share). TypeScript: "module": "ESNext" + "moduleResolution": "Bundler" cho modern projects.