ES Modules use static import/export resolved at parse time (enabling tree-shaking), while CommonJS uses dynamic require()/module.exports resolved at runtime — ESM is the modern standard, but both coexist in 2025.
Static import/export resolved at parse time; enables tree-shaking; strict mode by default; supports top-level await
Dynamic require()/module.exports resolved at runtime; synchronous loading; cached after first require; no tree-shaking
Named exports enable tree-shaking and explicit imports; default exports import entire module — prefer named for optimization
import() returns a Promise for on-demand loading — enables code splitting to reduce initial bundle size
ESM is the standard for new projects; CJS still powers millions of packages; both coexist via conditional exports in package.json
JavaScript modules let you split code into separate files with explicit dependencies. Two module systems dominate: ES Modules (ESM, the language standard) and CommonJS (CJS, the Node.js legacy system).
import { foo } from './module.js' and export function foo() {}.import statements cannot appear inside if blocks or functions.export default for a single primary export and named exports for multiple values.await is supported in ESM — you can await at the module's top level without wrapping in an async function."type": "module" in package.json or .mjs file extension.const foo = require('./module') and module.exports = { foo }.require() executes at runtime and can appear anywhere, including inside conditionals or loops.require() blocks until the module is fully loaded. Fine for server-side Node.js, problematic for browsers.require() caches modules after first load — subsequent require() calls return the cached object.export { foo, bar } — imported by exact name with destructuring: import { foo, bar } from './mod'.export default function() {} — imported with any name: import myFunc from './mod'.import('./module.js') returns a Promise and loads the module at runtime. Available in both ESM and CJS contexts.node_modules, .js, then index.js in directories.'./foo.js', not './foo').Key Interview Distinction: Static vs Dynamic
ESM imports are statically analyzable — the dependency graph is known before execution, enabling tree-shaking and faster startup. CJS imports are dynamic — require() can be conditional and computed at runtime, offering flexibility but preventing static optimization. This is the fundamental tradeoff and the most commonly asked distinction.
Fun Fact
JavaScript had no module system for its first 14 years. CommonJS (require) was created in 2009 for Node.js, AMD (define) was created for browsers, and the community spent years debating which was better. ES Modules (import/export) were standardized in 2015 but didn't ship in all browsers until 2018 — making it one of the longest-running feature rollouts in web history.