JavaScript Module Systems — A Practical History, Why They Exist, and How They Differ
As a developer, I was curious why JavaScript has so many ways to “import” and “export” code. CommonJS, AMD, UMD, ESM — at first, they all…
JavaScript Module Systems — A Practical History, Why They Exist, and How They Differ
As a developer, I was curious why JavaScript has so many ways to “import” and “export” code. CommonJS, AMD, UMD, ESM — at first, they all looked like mysterious acronyms doing the same thing. But once I started digging, I realized each of these module systems was created to solve a real problem at a specific time in JavaScript’s evolution.
Modules are how we break large codebases into smaller, reusable pieces. They help us manage dependencies, avoid naming collisions, and build applications that scale. But JavaScript didn’t always have modules — in fact, early JavaScript lived entirely in the global scope, making large projects fragile and hard to maintain. The journey from global scripts to standardized ES Modules tells a fascinating story of how the language (and web) matured.
TL;DR
- CommonJS (CJS): synchronous modules for Node (server-side). Simple
require()/module.exports. Node.js - AMD: asynchronous loader for browsers (RequireJS).
define()+ dependency list. requirejs.org - UMD: wrapper pattern to be compatible across environments (CJS, AMD, globals). GitHub
- ESM: the standardized, native
import/exportmodule system supported by browsers and Node; enables static analysis and tree-shaking. MDN Web Docs
The Problem: JavaScript Had No Native Modules
In the early days (pre-ES6), JavaScript ran only in browsers, and the browser:
- Loaded scripts via
<script>tags - Had everything in global scope
- Couldn’t “import” or “export” code safely
Example:
<script src="a.js"></script>
<script src="b.js"></script>
<script src="c.js"></script>
If all files define global variables — collisions, dependencies, and ordering became nightmares 😵💫
So the community invented module systems to organize code.
That’s why module systems were needed
Early JavaScript had only global scripts. That created naming collisions, fragile load-order dependencies, and maintainability issues for growing apps.
Module systems introduced:
- Encapsulation: avoid global scope pollution.
- Explicit dependencies: make code graphs clear and analyzable.
- Different loading strategies: synchronous loading (fine on servers) vs asynchronous loading (needed for browsers).
A short timeline / context
- CommonJS (Node ecosystem, ~2009) — server-oriented, synchronous
require(). Great for local filesystem loads. - AMD (RequireJS, early 2010s) — browser-friendly, asynchronous loading to avoid blocking page load.
- UMD (mid-2010s) — pragmatic wrapper to let one build work in Node, AMD loaders, or plain browser globals.
- ESM (ECMAScript 2015 / native modules) — a standard, native
import/exportmechanism that browsers and Node now support.
Core examples
1) CommonJS (CJS)
Used predominantly in Node. Synchronous require() and module.exports:
math.js
// CommonJS
function add(a, b) { return a + b; }
module.exports = { add };
main.js
const math = require('./math.js');
console.log(math.add(2, 3));
— Built for server-side JS (Node.js).
Goal: simple, synchronous module loading (like require() in other languages).
⚙️ How it works
- Uses
require()(synchronous import) - Uses
module.exports(to export stuff) - Works great on the server (local file system, no async fetch)
- Doesn’t work natively in browsers (they load files asynchronously)
✅ Pros
- Simple, widely used in Node.js
- Easy to understand
❌ Cons
- Synchronous (bad for browsers)
- Not natively supported by browsers
2) AMD (Asynchronous Module Definition)
Designed for browsers where network I/O must be asynchronous.
math.js
// AMD
define([], function() {
return { add: function(a, b) { return a + b; } };
});
main.js
define(['./math.js'], function(math) {
console.log(math.add(2, 3));
});
When used: in older browser apps that used RequireJS to load modules without blocking page load.
⚙️ How it works
- Uses
define(dependencies, factoryFunction) - Loads modules asynchronously (via
<script>or XHR) - Implemented in libraries like RequireJS
✅ Pros
- Works in browsers (no blocking)
- Loads dependencies in parallel
❌ Cons
- Verbose syntax
- Harder to read and debug
3) UMD (Universal Module Definition)
A wrapper pattern so a single file can be consumed as AMD, CommonJS, or a global variable:
math.js (UMD pattern simplified)
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define([], factory);
} else if (typeof module === 'object' && module.exports) {
// CommonJS
module.exports = factory();
} else {
// Browser global
root.math = factory();
}
}(this, function () {
return {
add: function (a, b) {
return a + b;
}
};
}));
main.js (browser):
<script src="math.js"></script>
<script>
console.log(math.add(2, 3));
</script>
main.js (Node):
const math = require('./math.js');
console.log(math.add(2, 3));
When used: libraries distributed as UMD could be dropped into different projects regardless of the loader in use.
✅ Pros
- Works everywhere (Node, AMD, Browser globals)
- Common for shared libraries (like Lodash, jQuery, etc.)
❌ Cons
- Complex boilerplate
- Hard to maintain manually
4) ESM (ECMAScript Modules)
Motivation was to , Add a native, standardized module system to JavaScript itself.
Introduced in ES6 (2015) → Now supported by browsers and Node.js.
math.js
// ESM
export function add(a, b) { return a + b; }
main.js
import { add } from './math.js';
console.log(add(2, 3));
Usage: Native in modern browsers via <script type="module"> and in Node when using ESM mode. ESM’s static structure enables tools to do tree-shaking and advanced optimizations. MDN Web Docs
⚙️ How it works
- Uses
import/export - Browser loads modules natively
- Supports static analysis → allows tree-shaking, prefetching, dependency graphs
- Works both in browsers and Node.js (with
"type": "module"in package.json)
✅ Pros
- Official JS standard (built into language)
- Asynchronous, efficient
- Enables tree-shaking, static analysis
- Simple, readable syntax
❌ Cons (minor)
- Older environments need transpiling
- Some complexity with mixed CJS/ESM interop
How they differ conceptually
- Load model: CJS is synchronous (server), AMD/ESM are asynchronous (browser-friendly). ESM supports both static
importand dynamicimport()for on-demand loading. - Syntax: CJS uses
require/module.exports; AMD usesdefine; ESM usesimport/export. UMD is a compatibility wrapper. - Static analysis: Only ESM is part of the language spec and is statically analyzable (helpful for bundlers and tree-shaking).
What problem did each solve?
- CommonJS: Solved modularity for server-side JavaScript where synchronous I/O is fine. It gave Node a simple module API.
- AMD: Solved the browser-specific problem of non-blocking, parallel module loading so pages didn’t stall on script downloads.
- UMD: Solved distribution problems — library authors could ship a single build that works in multiple environments.
- ESM: Solved the long-term need for a standard, native, statically analyzable module system that works across environments and enables optimizations (tree-shaking, preloading, tooling integration).
Usefulness today — where each still matters
- ESM is the future and present for new projects. Browsers support it natively; Node supports it (with
type: "module"or.mjs). It’s the go-to for modern libraries and apps because of static analysis and tooling benefits. - CommonJS remains important in Node land: many npm packages and legacy systems still use it. Interop patterns exist (Node provides ways to import CJS from ESM and vice versa).
- UMD is still seen in older library bundles you might include directly via
<script>tags (CDN copies). It’s useful for backwards-compatible library distribution. - AMD is now mostly legacy; you’ll encounter it in older codebases that used RequireJS. Modern apps prefer native ESM or bundlers that output UMD/ESM.
Modules are the foundation of maintainable code. The evolution from
CJS → AMD → UMD → ESM
reflects how the platform (browser vs server) and the needs of the ecosystem (non-blocking loads, distribution, and tooling) shaped the formats we use.
Today, ESM’s standardization simplifies the module story: native imports in browsers, static analysis for powerful optimizations, and a clearer path forward for tooling and libraries
Understanding JavaScript’s module systems isn’t just about syntax — it’s about seeing how the language grew with its community’s needs. From CommonJS powering early Node.js apps to ESM shaping modern frameworks like Vite and Next.js, each step reflects how developers solved the real challenges of sharing and organizing code.
As a developer, learning this evolution gave me a deeper appreciation of the tools I use every day. Now, when I write import or export, I know the decades of iteration and problem-solving behind that simple line — and that’s what makes modern JavaScript so powerful.
메타데이터
- post_id
- 7a2b77ece4cb
- slug
- javascript-module-systems-a-practical-history-why-they-exist-and-how-they-differ-7a2b77ece4cb
- url
- https://medium.com/@dev.cs.patial/javascript-module-systems-a-practical-history-why-they-exist-and-how-they-differ-7a2b77ece4cb
- canonical_url
- https://medium.com/@dev.cs.patial/javascript-module-systems-a-practical-history-why-they-exist-and-how-they-differ-7a2b77ece4cb
- author_url
- https://medium.com/@dev.cs.patial
- status
- ok
- fetched_at
- 2026-06-22 00:32:12