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. Lưu ý: 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.
ESM is the standard with static imports and tree shaking — CJS dynamic require() is not tree-shakeable; browsers natively support ESM; Vite dev server is fast because it serves native ESM without bundling; CJS cannot require() ESM (async).
CommonJS (CJS): require() is synchronous and dynamic (can be used inside if/functions); module.exports can be any value. Node.js default. Not tree-shakeable (bundlers can't know which exports are used due to dynamic nature). ESM: import/export are static (must be at top level), asynchronous loading, officially the JavaScript standard (TC39). Tree-shakeable. Supports top-level await and import.meta.url. Why ESM is winning: Tree shaking — ESM's static structure lets bundlers eliminate dead code (importing one function from lodash-es vs old lodash); Async loading — ESM modules load asynchronously, CJS is synchronously blocking; Standard — native browser support, Node.js support since v12; Better DX — Vite's native ESM dev server is extremely fast because it doesn't bundle. Current Node.js interop: CJS can require() CJS; ESM can import CJS (default export only); CJS cannot require() ESM (async) — must use dynamic import(). Dual package (publishing both CJS and ESM): use exports field in package.json with conditional exports. Pitfall: Dual package hazard — an app may load both CJS and ESM versions of the same package (2 instances, state is not shared). TypeScript: "module": "ESNext" + "moduleResolution": "Bundler" for modern projects.