ES Modules vs CommonJS

Difficulty: Intermediate

Question

What is the difference between ES Modules and CommonJS? When would you use each?

Answer

CommonJS (CJS): - Used in Node.js historically - require() is synchronous and dynamic - module.exports and exports object - Loaded at runtime

ES Modules (ESM): - Native browser module standard and modern Node.js - import/export syntax - Static analysis - imports resolved at parse time - Async loading - Enables tree shaking

Key differences: - ESM is static; CJS is dynamic (require() inside conditions works) - ESM has live bindings; CJS exports values by copy - ESM has top-level await; CJS does not - Node.js requires .mjs or type: module in package.json for ESM

Code examples

ESM vs CJS Syntax and Live Bindings

// CommonJS
const { readFile } = require('fs').promises;
module.exports = { greet: (name) => `Hello ${name}` };

// Dynamic require (only possible in CJS)
if (process.env.NODE_ENV === 'test') {
  const mock = require('./mocks');
}

// ES Modules
import { readFile } from 'fs/promises';
export const greet = (name) => `Hello ${name}`;
export default function main() {}

// Named vs default
import defaultExport from './module';
import { named } from './module';
import * as all from './module';

// Dynamic import (lazy loading)
const { heavy } = await import('./heavy-module.js');

// Top-level await (ESM only)
const config = await fetch('/config.json').then(r => r.json());
console.log(config.version);

ESM exports are live bindings - imported values reflect changes. CJS exports are copied values. Dynamic import() enables code splitting.

Tree Shaking

// utils.js with named exports (tree-shakeable)
export function formatDate(date) { return date.toISOString(); }
export function formatCurrency(n) { return `${n.toFixed(2)}`; }
export function parseQuery(str) { /* ... */ }

// Only formatDate imported - bundler eliminates others
import { formatDate } from './utils.js';

// Dynamic import for code splitting
document.querySelector('#loadChart').addEventListener('click', async () => {
  const { Chart } = await import('./chart.js');
  new Chart(document.querySelector('#canvas')).render(data);
});

// Check if in ESM vs CJS (Node.js)
const isESM = typeof __dirname === 'undefined';
const currentDir = isESM
  ? new URL('.', import.meta.url).pathname
  : __dirname;

Named exports are tree-shakeable - bundlers eliminate unused code statically. Dynamic import() enables route-level code splitting.

Key points

Concepts covered

ES Modules, CommonJS, import, require, Tree Shaking, Dynamic Import