What is the difference between ES Modules and CommonJS, and why does it matter?
Understanding the two module systems, static vs dynamic resolution, and live bindings.
CommonJS (require/module.exports) was Node's original system: it is synchronous and dynamic — require() runs at call time, resolves and executes the module, and returns a COPY of the exported values (a snapshot). Because it's dynamic you can require conditionally, but tools can't statically know the dependency graph. ES Modules (import/export) are the standard, and are static: imports/exports are resolved before execution by parsing, so the dependency graph is known ahead of time. That statically-analyzable structure is what enables tree-shaking (dead-export elimination) and is why import statements must be top-level. ESM exports are LIVE BINDINGS, not copies — if the exporting module reassigns an exported variable, importers see the new value; CJS would not. ESM is asynchronous-friendly and supports top-level await; dynamic import() returns a promise for code-splitting/lazy loading. Interop friction (naming, __dirname, JSON imports, the .mjs/.cjs/`type: module` rules) exists because the systems differ fundamentally. Rule of thumb: author ESM for new code (browser-native, tree-shakeable, future-proof); understand CJS for legacy Node and its require semantics.
Explaining bundler tree-shaking, code-splitting via dynamic import(), and debugging Node interop / live-binding surprises.