JavaScript modules solve the headaches of tangled codebases by letting developers split code into reusable, maintainable chunks. Today, two main systems dominate: CommonJS (CJS) and ES Modules (ESM). Let’s demystify the differences, explore practical use cases, and see how to migrate code the right way.
What Are JavaScript Modules?
Modules are files that encapsulate functionality, isolating variables and exports so code can be split up and reused, improving organization and maintainability
CommonJS
- Used in Node.js, relies on require() to import modules and module.exports to export.
- Loads modules synchronously (blocking).
Example:
ES Modules (ESM/ES6 Modules)
- Introduced in ES6, uses import and export syntax.
- Loads modules asynchronously, natively supported in browsers and recent Node versions.
Example:
Migrating from CommonJS to ES Modules
- Rename files to .mjs or set "type": "module" in package.json.
- Change require('./lib') to import { foo } from './lib.js'.
- Change module.exports = ... to export default ... or export { ... }.
- Refactor dynamic behaviors: ES Modules imports must be at the top-level and are static.
Example migration:
Why CommonJS Isn't Tree Shakable (but ESM Is)
- CommonJS: Dynamic; imports/exports are resolved at runtime, so bundlers can't safely remove unused code.
- ES Modules: Static; imports/exports are available at build time, allowing bundlers (like webpack) to remove unused ("dead") code for smaller builds.
Circular Dependency: CommonJS vs ESM
- CommonJS: Modules are loaded immediately. If two modules depend on each other, one may get an incomplete export.
- ES Modules: Modules are initialized but not executed until import is resolved (“live bindings”). This makes circular references safer and more predictable.
How import.meta Works in ES Module
- Provides metadata about the current module (like import.meta.url for path info).
- Helps replace CommonJS globals like __dirname/__filename.
Example:
'require' vs 'import' in Node.js & Browser
| Feature | CommonJS (require) | ES Modules (import) |
|---|---|---|
| Node.js Support | Fully supported | Supported since v12+ |
| Browser Support | Not natively | Native (modern browsers) |
| Syntax | require/module.exports |
import/export |
| Loading | Synchronous | Asynchronous |
| Use Case | Server-side | Client & server |
| Tree Shaking | No | Yes |
| Dynamic Import | require() |
import() |
Developer POV: Best Practices
- New projects: Use ES Modules for browser and server code.
- Migrating: Identify dynamic imports, refactor to static top-level imports before switching.
- Tools: Take advantage of tree shaking by using ESM; avoid mixing CommonJS and ESM in one module.
- Circular dependencies: Prefer ESM for safer live bindings.
- Modern Node.js: Most libraries and frameworks now use ESM—use "type": "module" in new projects.
Conclusion
Mastering modules is about writing clean, maintainable code and building faster, robust applications. If you want migration tips or deep-dive examples, just write down in comments below!