ES Modules Logo ESModules.com

History & Evolution

How JavaScript went from global script tags to a standardized native module system. Understanding the history helps you appreciate why ES Modules work the way they do.

The Script Tag Era (1995-2009)

JavaScript was created in 1995 with no module system. All scripts shared a single global scope, leading to naming conflicts and dependency chaos.

<!-- The original "module system" - order matters! -->
<script src="jquery.js"></script>
<script src="jquery-plugin.js"></script>
<script src="app.js"></script>

<!-- Problems: -->
<!-- 1. Everything in global scope (window.jQuery, window.$) -->
<!-- 2. Script order is critical - wrong order = errors -->
<!-- 3. No dependency declaration -->
<!-- 4. Name collisions between libraries -->

The IIFE Pattern (2003+)

Developers invented the Immediately Invoked Function Expression to create private scope:

// IIFE - Immediately Invoked Function Expression
var MyModule = (function() {
  // Private variables - not accessible outside
  var privateData = 'secret';

  // Public API - returned object
  return {
    getData: function() {
      return privateData;
    }
  };
})();

MyModule.getData(); // 'secret'
// privateData is not accessible directly

This was clever but manual - developers had to carefully manage the global namespace and dependency order.

Community Module Systems (2009-2015)

CommonJS / Node.js (2009)

Node.js adopted CommonJS, giving JavaScript its first practical module system. Synchronous loading worked well for servers but not browsers.

// math.js - CommonJS module
const PI = 3.14159;
function circleArea(r) {
  return PI * r * r;
}
module.exports = { circleArea, PI };

// app.js - require() is synchronous
const { circleArea } = require('./math');
console.log(circleArea(5)); // 78.54

Impact:

CommonJS enabled the npm ecosystem to explode. By 2026, npm hosts over 3 million packages. But the synchronous require() was fundamentally incompatible with browser needs.

AMD / RequireJS (2011)

AMD (Asynchronous Module Definition) solved the browser problem with async loading:

// AMD - Asynchronous Module Definition
define(['jquery', 'lodash'], function($, _) {
  // Module code here
  return {
    init: function() {
      $('.app').html(_.template('Hello'));
    }
  };
});

// RequireJS loaded modules asynchronously
// Good for browsers, but verbose syntax

UMD - The Universal Adapter (2014)

UMD (Universal Module Definition) tried to work everywhere - AMD, CommonJS, and global scope:

// UMD - works in AMD, CommonJS, and browser globals
(function(root, factory) {
  if (typeof define === 'function' && define.amd) {
    define(['dependency'], factory);       // AMD
  } else if (typeof module === 'object') {
    module.exports = factory(require('dependency')); // CJS
  } else {
    root.MyLib = factory(root.Dependency); // Browser global
  }
}(typeof self !== 'undefined' ? self : this, function(dep) {
  // Module code here
  return { /* public API */ };
}));

UMD was ugly but practical. Many libraries shipped UMD builds to maximize compatibility. Some still do today for legacy support.

The Bundler Revolution (2013-2020)

Browserify (2013)

Browserify brought CommonJS require() to the browser by bundling all dependencies into a single file:

# Bundle Node-style require() for browsers
browserify main.js -o bundle.js

// Now require() works in the browser!
const _ = require('lodash');
const myModule = require('./my-module');

webpack (2014)

webpack introduced the concept of a module bundler that could handle any asset type - not just JavaScript:

// webpack.config.js - bundles everything
module.exports = {
  entry: './src/index.js',
  output: { filename: 'bundle.js' },
  module: {
    rules: [
      { test: /\.css$/, use: ['style-loader', 'css-loader'] },
      { test: /\.png$/, use: 'file-loader' }
    ]
  }
};

// Code could now import anything
import styles from './app.css';
import logo from './logo.png';

webpack dominated JavaScript tooling for years and pioneered features like code splitting, hot module replacement, and tree-shaking.

Rollup (2015)

Rollup was the first bundler built specifically for ES Modules, pioneering tree-shaking (dead code elimination):

// Rollup leveraged ES Module static structure
// to eliminate unused code (tree-shaking)

// utils.js
export function used() { /* ... */ }
export function unused() { /* ... */ }

// app.js
import { used } from './utils.js';
// Rollup removes unused() from the final bundle!

Native ES Modules (2015-Present)

The Standard Arrives (ES2015)

ECMAScript 2015 finally specified a native module system with static import/export syntax:

// The syntax we know today - standardized in 2015
export function add(a, b) { return a + b; }
export default class Calculator { /* ... */ }

import Calculator, { add } from './math.js';

// Key design decisions:
// - Static structure (analyzable at compile time)
// - Asynchronous loading (browser-friendly)
// - Live bindings (not value copies)
// - Strict mode by default

Browser Support Timeline

Year Milestone
2015 ES2015 specification published with module syntax
2017 Safari 10.1 ships first browser ESM support; Chrome 61 follows
2018 Firefox 60, Edge 16 add support. Dynamic import() proposed
2019 Node.js 12 adds experimental ESM. Dynamic import() standardized
2020 Top-level await reaches Stage 3. Vite launches (ESM-native dev server)
2021 Node.js 16 stabilizes ESM. Top-level await standardized (ES2022)
2023 Import maps supported in all browsers. Deno, Bun go ESM-native
2025 Import attributes and JSON modules standardized (ES2025). Node 23 enables require(esm)
2026 Import defer and source phase imports advancing. Rust-based tooling (Rolldown, Oxc) becomes mainstream

The Modern Era (2024-2026)

ES Modules are now the undisputed standard across all JavaScript environments:

  • Universal runtime support - Node.js, Deno, Bun, browsers, and edge runtimes all support ESM natively
  • CJS interop solved - Node.js 23+ enables require() of ESM, ending the CJS/ESM divide
  • Import attributes (ES2025) - Native JSON and CSS module imports with the with keyword
  • Rust-based tooling - Rolldown, Oxc, Rspack, and Turbopack bring 10-30x speed improvements
  • Import maps universal - All browsers support import maps for bundler-free development
  • TypeScript native - Node.js 24+, Deno, and Bun run TypeScript directly

The Bottom Line:

After 11 years of evolution since ES2015, the JavaScript module story is settled. ES Modules are the universal standard. New projects should always use ESM, and the ecosystem provides excellent tooling for migrating legacy code.

Frequently Asked Questions