The module system you choose affects bundling, tree-shaking, and runtime behavior. Here's what I've learned shipping both at Google.
CommonJS (CJS)
CJS loads modules synchronously. require() is a function call — it can appear anywhere, even inside conditions. This makes static analysis impossible.
ES Modules (ESM)
ESM imports are static declarations. The engine can analyze the entire dependency graph before execution — this enables tree-shaking.
Key Differences
| Feature | CJS | ESM |
|---|---|---|
| Loading | Synchronous | Asynchronous |
| Binding | Value copy | Live binding |
| this | module.exports | undefined |
| Tree-shaking | Not possible | Supported |
| Top-level await | No | Yes |
The Live Binding Gotcha
Interop Challenges
- CJS
require()of ESM: Not supported (use dynamicimport()instead) - ESM
importof CJS: Works but you get the wholemodule.exportsas default __dirnamedoesn't exist in ESM — useimport.meta.url