The Need for Modularity
As JavaScript applications grew from simple scripts to complex systems, managing the global namespace became impossible. Modularity allows developers to break code into small, self-contained units that export specific functionality and keep their internal logic private.
Pre-Module Patterns: The IIFE
Before any formal module system existed, developers used Immediately Invoked Function Expressions (IIFEs) to create private scopes and avoid polluting the global object.
const counterModule = (function() {
let count = 0; // Private
return {
increment: () => ++count,
get: () => count
};
})();
CommonJS: The Node.js Standard
CommonJS (CJS) was developed for server-side environments like Node.js. It uses require() for importing and module.exports for exporting. It is synchronous, which is appropriate for server environments where modules are loaded from the local disk.
// math.js
const add = (a, b) => a + b;
module.exports = { add };
// index.js
const { add } = require('./math');
ESM: The Modern Standard
ECMAScript Modules (ESM) are the official, standardized module system for JavaScript, supported by both modern browsers and Node.js. ESM uses import and export statements.
Key Characteristics of ESM
- Static Analysis: Imports and exports are determined at compile-time/parsing-time, not runtime. This enables “Tree Shaking”—the ability for bundlers to remove unused code.
- Strict Mode: ESM always operates in strict mode by default.
- Asynchronous Loading: Modules can be loaded asynchronously, which is essential for web performance.
- Live Bindings: Modifying an exported variable in the source module updates it in the importing module (unlike CommonJS, which exports copies).
Export Types
- Named Exports: Multiple exports per module. Must be imported using the exact name.
- Default Export: Only one per module. Can be imported using any name.
Dynamic Imports
While most imports happen at the top of the file, ESM supports dynamic imports via the import() function. This returns a Promise and allows for “Lazy Loading”—loading code only when it is actually needed, which significantly reduces the initial bundle size of a web application.
if (userIsAdmin) {
const { loadDashboard } = await import('./admin.js');
loadDashboard();
}
Bundling and the Web
Browsers must download every module individually. For a large application with hundreds of modules, this creates massive network overhead. Tools like Webpack, Rollup, and Vite are used to “bundle” these modules into fewer, optimized files, while also handling transpilation (via Babel) and minification.