JS Guide
HomeQuestionsTopicsCompaniesResources
BookmarksSearch

Built for developers preparing for JavaScript, React & TypeScript interviews.

ResourcesQuestionsSupport
HomeQuestionsSearchProgress
HomeTopicsjavascriptModules
PrevNext
javascript
intermediate
7 min read

Modules

bundling
commonjs
esm
export
import
modules
tree-shaking

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.

Key Points

1ES Modules (ESM)

Static import/export resolved at parse time; enables tree-shaking; strict mode by default; supports top-level await

2CommonJS (CJS)

Dynamic require()/module.exports resolved at runtime; synchronous loading; cached after first require; no tree-shaking

3Named vs Default

Named exports enable tree-shaking and explicit imports; default exports import entire module — prefer named for optimization

4Dynamic Imports

import() returns a Promise for on-demand loading — enables code splitting to reduce initial bundle size

52025 Status

ESM is the standard for new projects; CJS still powers millions of packages; both coexist via conditional exports in package.json

What You'll Learn

  • Understand the difference between ES Modules and CommonJS
  • Know why ESM enables tree-shaking and CJS does not
  • Know how dynamic imports enable code splitting

Deep Dive

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).

ES Modules (ESM)

  • Syntax: import { foo } from './module.js' and export function foo() {}.
  • Imports are static — resolved at parse time before any code executes. This means import statements cannot appear inside if blocks or functions.
  • Static analysis enables tree-shaking: bundlers like Webpack and Rollup can detect and remove unused exports from the final bundle.
  • ESM runs in strict mode by default. Each module has its own scope — no global pollution.
  • Supports export default for a single primary export and named exports for multiple values.
  • Top-level await is supported in ESM — you can await at the module's top level without wrapping in an async function.
  • Enable in Node.js with "type": "module" in package.json or .mjs file extension.

CommonJS (CJS)

  • Syntax: const foo = require('./module') and module.exports = { foo }.
  • Imports are dynamic — require() executes at runtime and can appear anywhere, including inside conditionals or loops.
  • No tree-shaking by default because the dependency graph isn't known until runtime.
  • Modules are loaded synchronously — 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.
  • Still powers millions of npm packages and most existing Node.js codebases.

Named vs Default Exports

  • Named exports: export { foo, bar } — imported by exact name with destructuring: import { foo, bar } from './mod'.
  • Default export: export default function() {} — imported with any name: import myFunc from './mod'.
  • Prefer named exports for tree-shaking — default exports import the entire module, making it harder for bundlers to eliminate unused code.
  • You can mix both: a module can have one default export and multiple named exports.

Dynamic Imports

  • import('./module.js') returns a Promise and loads the module at runtime. Available in both ESM and CJS contexts.
  • Enables code splitting — load modules on demand instead of upfront. Critical for reducing initial bundle size in web applications.

Module Resolution

  • Node.js with CJS: looks for node_modules, .js, then index.js in directories.
  • Node.js with ESM: requires explicit file extensions in relative imports ('./foo.js', not './foo').
  • Bundlers (Webpack, Vite) add their own resolution logic, often allowing extensionless imports.

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.

Practice What You Learned

What is the difference between ES Modules (ESM) and CommonJS (CJS)?
mid
modules
ES Modules use import/export syntax with static analysis and are asynchronously loaded. CommonJS uses require/module.exports with synchronous loading. ESM is the modern standard for browsers and modern Node.js, while CJS is the legacy Node.js module system.
Explain the Module pattern and how ES6 modules differ from CommonJS.
senior
modules
The Module pattern encapsulates code using closures to create private state. ES6 modules are the standard with static imports (analyzed at compile time), while CommonJS uses dynamic require() at runtime. ES6 supports tree-shaking and async loading.
Previous
Proxy & Reflect
Next
Objects & Copying
PrevNext