Difficulty: Intermediate
What is the difference between ES Modules and CommonJS? When would you use each?
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
// 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.
// 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.
ES Modules, CommonJS, import, require, Tree Shaking, Dynamic Import