As JavaScript projects grow, organising code into modules becomes essential. But JavaScript has had two competing module systems for years — CommonJS (used in Node.js) and ES Modules (the modern standard). Add dynamic imports into the mix and it can get confusing fast. This article explains all three clearly, compares them side by side, and shows you exactly when to use each one.
Why modules exist
Before modules, all JavaScript code shared a single global scope. Every variable you declared could clash with any library you included. Modules solve this by giving each file its own private scope — you explicitly choose what to share and what to keep private.
| // Without modules — everything in global scope var userName = "Alice"; ← could clash with another script // With modules — private by default const userName = "Alice"; ← only visible in this file export { userName }; ← explicitly share if needed |
Part 1: CommonJS (CJS)
CommonJS was the original module system for Node.js. It uses require() to import and module.exports to export. It is synchronous — the entire file is loaded before execution continues.
Exporting with CommonJS
| // math.js function add(a, b) { return a + b; } function subtract(a, b) { return a - b; } const PI = 3.14159; // Export a single value module.exports = add; // Export multiple values as an object module.exports = { add, subtract, PI }; |
Importing with CommonJS
| // app.js const math = require("./math"); console.log(math.add(2, 3)); → 5 // Destructured import const { add, subtract, PI } = require("./math"); console.log(add(2, 3)); → 5 // Node built-in modules const fs = require("fs"); const path = require("path"); const http = require("http"); |
Part 2: ES Modules (ESM)
ES Modules are the official JavaScript standard, introduced in ES6. They use import and export keywords. Unlike CommonJS, ES modules are asynchronous and statically analysable — meaning bundlers can tree-shake unused code.
Named exports
| // math.js export function add(a, b) { return a + b; } export function subtract(a, b) { return a - b; } export const PI = 3.14159; // Or export at the bottom function multiply(a, b) { return a * b; } const E = 2.718; export { multiply, E }; |
Default export
| // Each file can have ONE default export export default function greet(name) { return `Hello, ${name}!`; } // Or an expression const config = { apiUrl: "https://api.example.com", timeout: 5000 }; export default config; |
Importing named and default exports
| // Named imports — must match export name exactly import { add, subtract, PI } from "./math.js"; // Rename during import import { add as sum, PI as pi } from "./math.js"; // Import everything as a namespace object import * as Math from "./math.js"; Math.add(2, 3); → 5 Math.PI; → 3.14159 // Default import — choose any name you like import greet from "./greet.js"; import myGreet from "./greet.js"; ← also valid — any name works // Both default and named in one line import config, { add, PI } from "./math.js"; |
| Named export vs default export: use named exports when a file exports multiple things. Use a default export when a file has one primary thing to export — like a React component or a class. Many style guides recommend avoiding default exports because they make refactoring harder. |
Re-exporting
| // index.js — barrel file that re-exports from multiple modules export { add, subtract } from "./math.js"; export { formatDate } from "./date.js"; export { fetchUser } from "./api.js"; // Now consumers import from one place import { add, formatDate, fetchUser } from "./index.js"; |
Part 3: Dynamic import()
Static imports (import ... from ...) must be at the top of a file and are always loaded. Dynamic import() lets you load a module on demand — only when needed. It returns a Promise.
Basic dynamic import
| // Static import — always loaded at startup import { heavyLibrary } from "./heavyLibrary.js"; // Dynamic import — loaded only when needed button.addEventListener("click", async () => { const { heavyLibrary } = await import("./heavyLibrary.js"); heavyLibrary.doSomething(); }); |
Conditional loading
| async function loadModule(type) { if (type === "chart") { const { renderChart } = await import("./chart.js"); renderChart(); } else if (type === "table") { const { renderTable } = await import("./table.js"); renderTable(); } } // Dynamic path — not possible with static import const lang = "vi"; const translations = await import(`./locales/${lang}.js`); |
Route-based code splitting (React / Vue pattern)
| // React lazy loading — only load page when user navigates there import React, { lazy, Suspense } from "react"; const Dashboard = lazy(() => import("./pages/Dashboard")); const Settings = lazy(() => import("./pages/Settings")); // Each page is only downloaded when the user visits it // Dramatically reduces initial bundle size |
CommonJS vs ES Modules — side by side
| Feature CommonJS (CJS) ES Modules (ESM) ────────────────── ─────────────────────── ──────────────────── Syntax require() / module.exports import / export Loading Synchronous Asynchronous Where used Node.js (mainly) Browsers + Node.js Tree shaking No Yes Dynamic paths Yes Only with import() Top-level await No Yes File extension .js .js or .mjs |
| Modern recommendation: use ES Modules for all new projects — browser code, Node.js 14+, and any project using a bundler like Vite or Webpack. CommonJS is still widely used in older Node.js projects and npm packages, so you will encounter both. |
Key takeaways
Modules give each file its own private scope — you explicitly export what you want to share and import what you need. CommonJS uses require() and module.exports — synchronous, Node.js default, still widely used. ES Modules use import and export — the modern standard for both browsers and Node.js, supports tree shaking. Named exports are imported by exact name and support renaming. Default exports can be imported with any name. Dynamic import() loads modules on demand and returns a Promise — use it for code splitting, conditional loading, and performance optimisation. Barrel files (index.js that re-exports everything) keep imports clean in large projects.