CommonJS (CJS): require() load đồng bộ và blocking, nên gọi được ở bất cứ đâu — kể cả trong if hay trong function. module.exports gán được bất kỳ giá trị gì, đổi lại là không tree-shaking được.
ESM: import bắt buộc ở top-level để bundler phân tích tĩnh được, load bất đồng bộ nên fetch song song, và tree-shakeable vì bundler biết chính xác export nào có dùng. Ba thứ chỉ ESM mới có: top-level await, import.meta.url, và import() động.
Chỗ hai bên va nhau:
- CJS require() một file ESM: không được, phải dùng import() động.
- ESM import một file CJS: được, nhưng chỉ lấy được default import.
- Dual package hazard: publish cả CJS lẫn ESM trong cùng package, app lỡ nạp cả hai thì có 2 instance của module — state và class không dùng chung được nữa. Cách chữa là conditional exports trong package.json.
Lưu ý: đặt type: 'module' trong package.json nghĩa là mọi file .js thành ESM, file CJS còn lại phải đổi đuôi .cjs. Node.js 22+ có cờ --experimental-require-module để require() được ESM.