ESM vs CommonJS: What Actually Differs Under the Hood
Both module systems run JavaScript, but they load and execute code differently. Here are the differences that bite in production. ES Modules and CommonJS both let you split code into files and share functions between them, but the mechanics are fundamentally different. CommonJS is synchronous and dynamic; ESM is asynchronous and static. These differences affect performance, tooling, and which patterns even work. I migrated a large codebase from CommonJS to ESM, and the surprises came from exactly these mechanical differences. Static vs Dynamic Structure ESM imports are static: the import specifiers must be statically analyzable string literals, and the bindings are live references to the exported values. This lets bundlers tree-shake unused exports because they can see, at build time, which symbols are imported. CommonJS require can be called anywhere with any expression, so static analysis is approximate at best, and tree shaking is unreliable. The live binding behavior of ESM is subtle. When you import a value from an ESM module, you get a reference that updates if the exporting module reassigns that export. With CommonJS, require returns a snapshot of module.exports at the moment require ran; later reassignments…