Fundamentals 13 min read

A Comprehensive Overview of JavaScript Module Systems: From Traditional Scripts to ESM

This article traces the evolution of JavaScript module systems—from the early script tags and manual dependency ordering, through CommonJS, AMD, and UMD, to the modern ECMAScript modules—explaining their origins, challenges, and how bundlers and package fields enable seamless usage across browsers and Node.js environments.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
A Comprehensive Overview of JavaScript Module Systems: From Traditional Scripts to ESM

If you have examined modern JavaScript libraries, you may be puzzled by the myriad module adaptation strategies; this article reviews the history and current state of JavaScript modularization, explaining why each system was created and what problems it solves.

Traditional JavaScript

Initially JavaScript ran only in browsers without any module system; scripts were loaded with simple <script src="..."></script> tags, requiring developers to manually order dependencies, which quickly became painful for larger projects.

Node.js Module Solution

Node.js introduced the CommonJS specification, making modules mandatory. A typical CommonJS module looks like:

define(function (require, exports, module) {
    // use event module
    var ec = require("event");
});

Many libraries still provide only a CommonJS entry point.

Browser Module Solutions

Because CommonJS is synchronous and unsuitable for browsers, the AMD (Asynchronous Module Definition) standard was created, typically used with the RequireJS loader. An AMD module is defined as:

define(["beta"], function (beta) {
    // call module
});

AMD saw wide adoption but has largely fallen out of use.

Bridging the Gap: UMD

To allow a library to work in any environment, the Universal Module Definition (UMD) pattern was devised. A typical UMD wrapper detects AMD, CommonJS, or falls back to a global variable:

(function (root, factory) {
  var Data = factory(root);
  if (typeof define === 'function' && define.amd) {
    define('data', function () { return Data; });
  } else if (typeof exports === 'object') {
    module.exports = Data;
  } else {
    var _Data = root.Data;
    Data.noConflict = function () { if (root.Data === Data) { root.Data = _Data; } return Data; };
    root.Data = Data;
  }
}(this, function (root) {
  var Data = ...;
  // library code
  return Data;
}));

Even after UMD, the newer ECMAScript Module (ESM) standard introduced in ES2015 reshaped the landscape.

ESM and Build Tools

ESM provides native import and export syntax, but browsers and Node.js required tooling for full support. Early bundlers like Rollup added dual entry points (ESM and CommonJS). Webpack later added mainFields configuration to prioritize module over main . Example configuration screenshots are shown in the original article.

Handling Environment Differences

Libraries that need to run both in browsers and Node.js often use the browser field in package.json to replace Node‑specific modules with browser equivalents. Tools such as Webpack respect this field.

Node.js Support for ESM

Node.js supports ESM via file extensions (.mjs) or the type field in package.json . It can import CommonJS modules, but CommonJS cannot import ESM directly. The newer exports field further refines entry point selection for different environments.

Dual‑Package Pitfalls

Providing both ESM and CommonJS entries can lead to the “dual‑package” problem where the same library is executed twice if one dependency loads the ESM version while another loads the CommonJS version. Solutions include making the CommonJS version the core implementation and having the ESM version act as a thin wrapper, or dropping CommonJS support entirely.

Deno and URL‑Based Loading

Deno loads modules via URLs, and can consume npm packages through services like unpkg or esm, which require additional fields in package.json to map URLs to the correct entry points.

Conclusion

The article summarizes the major JavaScript module systems, their historical motivations, and how modern tooling bridges the gaps, enabling developers to use any module format seamlessly across browsers and server environments.

JavaScriptNode.jsCommonJSESMModulesAMDBundlers
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.