24.4-ES-Modules-Internals
22.4 ES Modules Internals
Understanding how ES Modules work internally helps you write better module code, avoid circular dependency issues, and optimize load performance.
Table of Contents
- β’Module System Overview
- β’Module Loading Phases
- β’Module Records and Environments
- β’Circular Dependencies
- β’Dynamic Imports
- β’Module Caching
- β’Top-Level Await
- β’CommonJS vs ES Modules
- β’All JavaScript Module Systems
- β’IIFE Module Pattern
- β’CommonJS (CJS)
- β’AMD
- β’UMD
- β’ES6 Modules (ESM)
- β’NPM Package Modules
- β’Bundler-Based Modules
- β’Babel-Transpiled Modules
- β’Top-Level Await
Module System Overview
ES Modules are JavaScript's native module system, designed for static analysis and optimization.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ES MODULES OVERVIEW β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β KEY CHARACTERISTICS: β
β β
β 1. STATIC STRUCTURE β
β β’ Imports/exports determined at compile time β
β β’ Enables tree-shaking and dead code elimination β
β β’ Cannot conditionally import (at top level) β
β β
β 2. LIVE BINDINGS β
β β’ Imports are references, not copies β
β β’ Changes in exporter reflect in importer β
β β
β 3. SINGLETON MODULES β
β β’ Each module executed only once β
β β’ Cached for all subsequent imports β
β β
β 4. STRICT MODE BY DEFAULT β
β β’ All ES modules run in strict mode β
β β’ No need for 'use strict' β
β β
β 5. ASYNCHRONOUS LOADING β
β β’ Modules can be loaded in parallel β
β β’ Execution happens after all dependencies ready β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Static vs Dynamic
// β
STATIC - Analyzed at compile time
import { foo } from './module.js';
export const bar = 42;
// β NOT STATIC - These are errors at top level
// if (condition) {
// import { foo } from './module.js'; // Error!
// }
// β
DYNAMIC - Use dynamic import for runtime decisions
if (condition) {
const { foo } = await import('./module.js'); // OK!
}
Module Loading Phases
ES Module loading happens in three distinct phases.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MODULE LOADING PHASES β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β PHASE 1: CONSTRUCTION (Parsing) β
β ββββββββββββββββββββββββββββββββ β
β β’ Fetch module source code β
β β’ Parse the source code β
β β’ Find import/export statements β
β β’ Create Module Record (metadata) β
β β’ Build dependency graph β
β β
β βββββββββββ fetch βββββββββββ parse β
β β URL β ββββββββββββ β Source β ββββββββββββ Module β
β βββββββββββ βββββββββββ Record β
β β
β PHASE 2: INSTANTIATION (Linking) β
β βββββββββββββββββββββββββββββββ β
β β’ Allocate memory for all exports β
β β’ Link imports to exports (live bindings) β
β β’ NO CODE EXECUTED YET β
β β
β Module A βββββββββββ exports βββββββββββ binding β
β β β β
β βββββββββββββ imports ββββββββββββββββββ β
β β
β PHASE 3: EVALUATION (Execution) β
β βββββββββββββββββββββββββββββββββ β
β β’ Execute module code (top-level) β
β β’ Fill export bindings with values β
β β’ Depth-first, post-order execution β
β β
β Dependency tree: Execution order: β
β A C (first) β
β / \ D (second) β
β B C B (third) β
β | A (last) β
β D β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Phase Details
// main.js
import { value, increment } from './counter.js';
console.log(value); // What happens here?
increment();
console.log(value); // And here?
// counter.js
export let value = 0;
export function increment() {
value++;
}
PHASE 1: CONSTRUCTION
ββββββββββββββββββββ
1. Parse main.js β finds import from './counter.js'
2. Fetch and parse counter.js
3. Build Module Records for both
PHASE 2: INSTANTIATION
βββββββββββββββββββββ
1. Allocate memory for counter.js exports (value, increment)
2. Link main.js imports to counter.js exports
3. Both modules ready, no code run yet
PHASE 3: EVALUATION
ββββββββββββββββββ
1. Execute counter.js first (dependency)
- value = 0
- increment function created
2. Execute main.js
- console.log(value) β 0
- increment() β value becomes 1
- console.log(value) β 1 (live binding!)
Module Records and Environments
The JavaScript engine creates internal structures to manage modules.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MODULE RECORD β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β [[Realm]] The realm this module belongs to β
β [[Environment]] Module's lexical environment β
β [[Namespace]] Module namespace object β
β [[Status]] unlinked | linking | linked | β
β evaluating | evaluated | error β
β [[RequestedModules]] List of module specifiers β
β [[ImportEntries]] List of import entries β
β [[LocalExportEntries]] List of local export entries β
β [[StarExportEntries]] Re-exports with * β
β β
β Example for: β
β ββββββββββββ β
β // module.js β
β import { x } from './dep.js'; β
β export const y = x + 1; β
β export function double(n) { return n * 2; } β
β β
β ImportEntries: [{ β
β ModuleRequest: './dep.js', β
β ImportName: 'x', β
β LocalName: 'x' β
β }] β
β β
β LocalExportEntries: [ β
β { LocalName: 'y', ExportName: 'y' }, β
β { LocalName: 'double', ExportName: 'double' } β
β ] β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Module Namespace Object
// module.js
export const a = 1;
export const b = 2;
export function c() {
return 3;
}
// main.js
import * as mod from './module.js';
console.log(mod);
// Module Namespace Object:
// {
// a: 1,
// b: 2,
// c: [Function: c],
// [Symbol.toStringTag]: 'Module'
// }
console.log(Object.keys(mod)); // ['a', 'b', 'c']
console.log(Object.isFrozen(mod)); // true (in some environments)
Circular Dependencies
ES Modules handle circular dependencies, but understanding how is crucial.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CIRCULAR DEPENDENCIES β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β The Problem: β
β β
β a.js ββimportsβββ b.js β
β β β β
β βββββimportsββββββββ β
β β
β How ES Modules handle it: β
β β
β 1. Start evaluating a.js β
β 2. Hit import from b.js β
β 3. Pause a.js, start evaluating b.js β
β 4. b.js tries to import from a.js β
β 5. a.js already started β use CURRENT state β
β (even though a.js hasn't finished!) β
β 6. Finish b.js evaluation β
β 7. Continue a.js evaluation β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Circular Dependency Example
// a.js
import { b } from './b.js';
console.log('a.js: b =', b);
export let a = 'a after';
console.log('a.js: a =', a);
// b.js
import { a } from './a.js';
console.log('b.js: a =', a); // undefined! a.js not finished yet
export let b = 'b after';
console.log('b.js: b =', b);
Execution trace when running a.js:
1. Start a.js
2. Hit "import { b } from './b.js'"
3. Pause a.js (a is allocated but NOT initialized)
4. Start b.js
5. b.js: "import { a } from './a.js'" β a.js already started
6. b.js: console.log('b.js: a =', a) β "a" is UNDEFINED
7. b.js: export b = 'b after'
8. b.js: console.log('b.js: b =', b) β "b after"
9. b.js DONE
10. Resume a.js
11. a.js: console.log('a.js: b =', b) β "b after"
12. a.js: export a = 'a after'
13. a.js: console.log('a.js: a =', a) β "a after"
Output:
b.js: a = undefined
b.js: b = b after
a.js: b = b after
a.js: a = a after
Avoiding Circular Dependency Issues
// β BAD: Accessing export before initialization
// a.js
import { b, getB } from './b.js';
export const a = b + 1; // b might be undefined!
// β
GOOD: Use functions that are called later
// a.js
import { getB } from './b.js';
export const a = 'a';
export function getA() {
return a;
}
export function init() {
console.log('b is:', getB()); // Called after both modules loaded
}
// b.js
import { getA } from './a.js';
export const b = 'b';
export function getB() {
return b;
}
export function init() {
console.log('a is:', getA());
}
Dynamic Imports
Dynamic imports allow loading modules at runtime.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β DYNAMIC IMPORTS β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β SYNTAX: β
β β
β const module = await import('./module.js'); β
β β β
β βββ Returns Promise<ModuleNamespace> β
β β
β USE CASES: β
β β
β 1. Code Splitting β
β const { heavyFunction } = await import('./heavy.js'); β
β β
β 2. Conditional Loading β
β if (needsFeature) { β
β const { feature } = await import('./feature.js'); β
β } β
β β
β 3. Computed Module Paths β
β const lang = 'en'; β
β const i18n = await import(`./locales/${lang}.js`); β
β β
β 4. Lazy Loading β
β button.onclick = async () => { β
β const { handleClick } = await import('./handler.js'); β
β handleClick(); β
β }; β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Dynamic Import Patterns
// Pattern 1: Lazy Loading Components
async function loadComponent(name) {
const components = {
header: () => import('./components/header.js'),
footer: () => import('./components/footer.js'),
sidebar: () => import('./components/sidebar.js'),
};
return await components[name]?.();
}
// Pattern 2: Feature Detection
async function loadPolyfill() {
if (!('IntersectionObserver' in window)) {
await import('./polyfills/intersection-observer.js');
}
}
// Pattern 3: Parallel Loading
async function loadAllFeatures() {
const [featureA, featureB, featureC] = await Promise.all([
import('./features/a.js'),
import('./features/b.js'),
import('./features/c.js'),
]);
return { featureA, featureB, featureC };
}
// Pattern 4: Fallback Loading
async function loadWithFallback() {
try {
return await import('./feature-modern.js');
} catch {
return await import('./feature-legacy.js');
}
}
Module Caching
Modules are cached after first loadβunderstanding this is important.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MODULE CACHE β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Module Map (Browser/Runtime maintains this): β
β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β URL / Specifier β Module Record β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€ β
β β './counter.js' β { exports: { count, inc } } β β
β β './utils.js' β { exports: { helper } } β β
β β 'lodash' β { exports: { ... } } β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β First import('./counter.js'): β
β 1. Check cache β Not found β
β 2. Fetch, parse, instantiate, evaluate β
β 3. Store in cache β
β β
β Second import('./counter.js'): β
β 1. Check cache β FOUND β
β 2. Return cached module namespace β
β 3. NO re-execution β
β β
β Implications: β
β β’ Module code runs ONCE β
β β’ Module state is SHARED β
β β’ Same object/function references across all imports β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Singleton Pattern via Modules
// counter.js - This is a singleton!
let count = 0;
export function getCount() {
return count;
}
export function increment() {
count++;
}
// fileA.js
import { getCount, increment } from './counter.js';
increment();
console.log(getCount()); // 1
// fileB.js
import { getCount, increment } from './counter.js';
// Same module instance as fileA.js!
console.log(getCount()); // 1 (not 0!)
increment();
console.log(getCount()); // 2
Cache Busting (for Testing/Development)
// Adding a query string creates a "new" module
const counter1 = await import('./counter.js?v=1');
const counter2 = await import('./counter.js?v=2');
// counter1 and counter2 are DIFFERENT instances!
// Note: This is a workaround, not standard behavior
Top-Level Await
ES2022 allows await at the module's top level.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β TOP-LEVEL AWAIT β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Before (ES2015): β
β ββββββββββββββββ β
β // config.js β
β export let config; β
β β
β (async () => { β
β config = await loadConfig(); β
β })(); β
β β
β // Problem: config might be undefined when imported! β
β β
β With Top-Level Await (ES2022): β
β βββββββββββββββββββββββββββββ β
β // config.js β
β export const config = await loadConfig(); β
β β
β // Guaranteed: config is ready when module finishes loading β
β β
β Execution Order with TLA: β
β β
β main.js β
β β β
β import β
β β β
β βΌ β
β config.js ββββ await loadConfig() ββββ β
β β β β
β β β (async operation) β
β β β β
β βΌ βββββββββββββββββββββββββββββββ β
β continue main.js β
β β
β The importing module WAITS for the async module to complete! β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Top-Level Await Examples
// database.js
// Module evaluation waits for connection
const connection = await connectToDatabase();
export { connection };
export async function query(sql) {
return connection.query(sql);
}
// app.js
import { connection, query } from './database.js';
// Connection is guaranteed to be established here
console.log('Connected:', connection.isConnected);
const users = await query('SELECT * FROM users');
TLA Considerations
// β οΈ TLA can block loading of dependent modules
// config.js
export const config = await fetch('/api/config').then((r) => r.json());
// β This blocks ALL modules that import config.js
// β
Better: Use TLA sparingly
// Consider if async initialization is really needed at import time
// Sometimes lazy initialization is better
// Alternative: Lazy initialization
let _config = null;
export async function getConfig() {
if (!_config) {
_config = await fetch('/api/config').then((r) => r.json());
}
return _config;
}
CommonJS vs ES Modules
Understanding the differences helps when working with both systems.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CJS vs ESM COMPARISON β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β CommonJS ES Modules β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β Syntax require() import/export β
β module.exports export default/named β
β β
β Loading Synchronous Asynchronous β
β (blocking) (non-blocking) β
β β
β Resolution Runtime Static (compile time) β
β Dynamic Known before execution β
β β
β Bindings Value copy Live bindings β
β (snapshot) (reference) β
β β
β Evaluation Immediate Deferred β
β when required after all parsing β
β β
β Top-level Sync only Sync + Async (TLA) β
β β
β this value module object undefined β
β β
β Tree-shaking Not possible Possible β
β (dynamic) (static analysis) β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Live Bindings vs Value Copy
// === CommonJS (Value Copy) ===
// counter.cjs
let count = 0;
module.exports = {
count, // Copies current value
increment: () => count++,
};
// main.cjs
const counter = require('./counter.cjs');
console.log(counter.count); // 0
counter.increment();
console.log(counter.count); // Still 0! (value was copied)
// === ES Modules (Live Binding) ===
// counter.mjs
export let count = 0;
export function increment() {
count++;
}
// main.mjs
import { count, increment } from './counter.mjs';
console.log(count); // 0
increment();
console.log(count); // 1 (live binding - sees updated value)
Interoperability
// ESM importing CJS (usually works)
import lodash from 'lodash'; // CJS module
import { map } from 'lodash'; // May not work for named exports
// CJS importing ESM (requires dynamic import)
// const esm = require('./esm-module.mjs'); // β Error!
const esm = await import('./esm-module.mjs'); // β
Works
// Package.json configuration
{
"type": "module", // Treat .js as ESM
"exports": {
"import": "./esm/index.js",
"require": "./cjs/index.cjs"
}
}
All JavaScript Module Systems
A comprehensive overview of all module systems in JavaScript history.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β JAVASCRIPT MODULE SYSTEMS TIMELINE β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β 1999 βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β IIFE Module Pattern (Pre-modules) β β
β β First attempt at encapsulation β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β 2009 βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β CommonJS (CJS) β β
β β Node.js server-side modules β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β 2011 βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β AMD (Asynchronous Module Definition) β β
β β Browser async loading (RequireJS) β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β 2012 βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β UMD (Universal Module Definition) β β
β β Works everywhere (CJS + AMD + Global) β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β 2015 βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β ES6 Modules (ESM) β β
β β Native JavaScript modules β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β 2016+ βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Bundler Modules (Webpack/Rollup/Vite) β β
β β Build-time module resolution β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
1. IIFE Module Pattern (Old Module Pattern)
The original way to create private scope in JavaScript.
// IIFE (Immediately Invoked Function Expression) Module Pattern
const MyModule = (function () {
// Private variables
let privateVar = 0;
const privateConst = 'secret';
// Private function
function privateFunction() {
return privateVar * 2;
}
// Public API (Revealing Module Pattern)
return {
increment: function () {
privateVar++;
},
getCount: function () {
return privateVar;
},
getDouble: function () {
return privateFunction();
},
};
})();
// Usage
MyModule.increment();
console.log(MyModule.getCount()); // 1
console.log(MyModule.privateVar); // undefined (private!)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β IIFE MODULE PATTERN β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β PROS: β
β β
No dependencies or build tools needed β
β β
Works in all browsers β
β β
Creates true private scope β
β β
Immediately executable β
β β
β CONS: β
β β No dependency management β
β β Relies on script order in HTML β
β β No async loading β
β β Global namespace pollution (module name) β
β β Outdated for modern development β
β β
β USE CASE: Legacy code, quick scripts, learning closures β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
2. CommonJS (CJS)
The module system created for Node.js.
// math.js (CommonJS module)
const PI = 3.14159;
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
// Export multiple items
module.exports = {
PI,
add,
multiply,
};
// Or export individually
// exports.add = add;
// exports.PI = PI;
// main.js (CommonJS consumer)
const math = require('./math');
console.log(math.add(2, 3)); // 5
console.log(math.PI); // 3.14159
// Destructuring import
const { add, multiply } = require('./math');
console.log(multiply(4, 5)); // 20
// Conditional/dynamic require
if (needsMath) {
const dynamicMath = require('./math');
}
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β COMMONJS β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β SYNTAX: β
β require('./module') // Import β
β module.exports = { ... } // Export object β
β exports.name = value // Export individual β
β β
β CHARACTERISTICS: β
β β’ Synchronous loading (blocking) β
β β’ Runtime module resolution β
β β’ Exports are value COPIES (not live bindings) β
β β’ Dynamic imports allowed anywhere β
β β’ Circular dependencies: partial exports β
β β
β PROS: β
β β
Simple syntax β
β β
Huge NPM ecosystem β
β β
Dynamic requires possible β
β β
Well-understood in Node.js β
β β
β CONS: β
β β Synchronous (bad for browsers) β
β β No static analysis (no tree-shaking) β
β β Not native to browsers β
β β Being phased out for ESM in Node.js β
β β
β FILE EXTENSIONS: .js (with type: "commonjs") or .cjs β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
3. AMD (Asynchronous Module Definition)
Created for browsers to load modules asynchronously.
// math.js (AMD module)
define('math', [], function () {
const PI = 3.14159;
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
// Return public API
return {
PI: PI,
add: add,
multiply: multiply,
};
});
// main.js (AMD consumer)
define(['math', 'utils'], function (math, utils) {
// Dependencies are injected as arguments
console.log(math.add(2, 3));
console.log(utils.format('Hello'));
});
// Or using require
require(['math'], function (math) {
console.log(math.PI);
});
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β AMD β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β SYNTAX: β
β define(id?, deps?, factory) // Define module β
β require(deps, callback) // Load modules β
β β
β IMPLEMENTATIONS: β
β β’ RequireJS (most popular) β
β β’ curl.js β
β β’ Dojo Toolkit β
β β
β CHARACTERISTICS: β
β β’ Asynchronous loading β
β β’ Browser-first design β
β β’ Callback-based API β
β β’ Explicit dependency declaration β
β β
β PROS: β
β β
Async loading (non-blocking) β
β β
Works in browsers without build step β
β β
Parallel loading of dependencies β
β β
Good for large applications β
β β
β CONS: β
β β Verbose syntax β
β β Requires RequireJS library β
β β Mostly obsolete (replaced by ESM + bundlers) β
β β Complex configuration β
β β
β STATUS: Largely obsolete, replaced by ES Modules β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
4. UMD (Universal Module Definition)
Works in CommonJS, AMD, and browser globals.
// math.js (UMD module)
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['exports'], factory);
} else if (
typeof exports === 'object' &&
typeof exports.nodeName !== 'string'
) {
// CommonJS
factory(exports);
} else {
// Browser globals
factory((root.math = {}));
}
})(typeof self !== 'undefined' ? self : this, function (exports) {
// Module code here
exports.PI = 3.14159;
exports.add = function (a, b) {
return a + b;
};
exports.multiply = function (a, b) {
return a * b;
};
});
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β UMD β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β PURPOSE: Universal compatibility wrapper β
β β
β WORKS IN: β
β β Node.js (CommonJS) β
β β RequireJS (AMD) β
β β Browser script tags (global) β
β β
β DETECTION ORDER: β
β 1. Check for AMD (define.amd) β
β 2. Check for CommonJS (exports object) β
β 3. Fall back to browser global β
β β
β PROS: β
β β
Maximum compatibility β
β β
Single file works everywhere β
β β
Good for libraries β
β β
β CONS: β
β β Very verbose boilerplate β
β β Complex to write manually β
β β Mostly generated by bundlers now β
β β Being replaced by ESM + CJS dual packages β
β β
β STATUS: Still used for library distribution, but declining β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
5. ES6 Modules (ESM) - The Standard
Native JavaScript modules (covered in detail above).
// math.mjs (ES Module)
export const PI = 3.14159;
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
// Default export
export default class Calculator {
add(a, b) {
return a + b;
}
}
// main.mjs (ES Module consumer)
import Calculator, { PI, add, multiply } from './math.mjs';
import * as math from './math.mjs';
console.log(add(2, 3)); // Named import
console.log(math.PI); // Namespace import
const calc = new Calculator(); // Default import
// Dynamic import
const module = await import('./math.mjs');
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ES6 MODULES (ESM) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β SYNTAX: β
β import { x } from './mod.js' // Named import β
β import x from './mod.js' // Default import β
β import * as mod from './mod' // Namespace import β
β export const x = 1 // Named export β
β export default x // Default export β
β export { x, y } // Export list β
β β
β THE STANDARD: β
β β’ Part of ECMAScript specification β
β β’ Native browser support β
β β’ Native Node.js support (v12+) β
β β’ Static structure enables optimization β
β β
β KEY FEATURES: β
β β Static analysis / tree-shaking β
β β Live bindings (not copies) β
β β Async loading β
β β Top-level await β
β β Strict mode by default β
β β
β FILE EXTENSIONS: .mjs or .js with "type": "module" β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
6. NPM Package Module System
How npm packages expose modules.
// package.json
{
"name": "my-library",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
},
"./utils": {
"import": "./dist/utils.mjs",
"require": "./dist/utils.cjs"
}
},
"files": ["dist"]
}
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β NPM PACKAGE EXPORTS β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β PACKAGE.JSON FIELDS: β
β β
β "type": "module" β Treat .js as ESM β
β "type": "commonjs" β Treat .js as CJS (default) β
β β
β "main" β CJS entry point (legacy) β
β "module" β ESM entry point (bundlers) β
β "browser" β Browser-specific entry β
β "exports" β Modern entry points (recommended) β
β β
β EXPORTS FIELD (Modern): β
β ββββββββββββββββββββββββ β
β "exports": { β
β ".": { // Main entry β
β "import": "...", // ESM import β
β "require": "...", // CJS require β
β "types": "...", // TypeScript β
β "default": "..." // Fallback β
β }, β
β "./subpath": {...} // Subpath exports β
β } β
β β
β DUAL PACKAGE PATTERN: β
β Publish both ESM and CJS for maximum compatibility β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
7. Bundler-Based Modules
Modern bundlers transform and optimize modules.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β BUNDLER-BASED MODULES β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β BUNDLERS: β
β β
β WEBPACK β
β ββ Most configurable β
β ββ Code splitting, lazy loading β
β ββ Loaders for any file type β
β ββ Large plugin ecosystem β
β β
β ROLLUP β
β ββ Best tree-shaking β
β ββ Optimized for libraries β
β ββ ESM-first design β
β ββ Smaller output bundles β
β β
β VITE β
β ββ Uses native ESM in development β
β ββ Extremely fast HMR β
β ββ Rollup for production builds β
β ββ Zero-config for most projects β
β β
β PARCEL β
β ββ Zero configuration β
β ββ Built-in transforms β
β ββ Good for quick prototypes β
β β
β esbuild β
β ββ Extremely fast (Go-based) β
β ββ Basic but efficient β
β ββ Often used by other tools β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Webpack example - webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [{ test: /\.js$/, use: 'babel-loader' }],
},
optimization: {
splitChunks: { chunks: 'all' },
},
};
// Vite example - vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
build: {
rollupOptions: {
input: 'src/main.js',
output: {
manualChunks: {
vendor: ['lodash', 'axios'],
},
},
},
},
});
8. Babel-Transpiled Modules
Babel transforms modern module syntax for older environments.
// Input: ES Modules
import { add } from './math.js';
export const result = add(1, 2);
// Output: CommonJS (after Babel)
('use strict');
Object.defineProperty(exports, '__esModule', { value: true });
exports.result = void 0;
var _math = require('./math.js');
var result = (0, _math.add)(1, 2);
exports.result = result;
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β BABEL MODULE TRANSFORMS β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β COMMON TRANSFORMS: β
β β
β @babel/plugin-transform-modules-commonjs β
β β ESM to CJS β
β β
β @babel/plugin-transform-modules-amd β
β β ESM to AMD β
β β
β @babel/plugin-transform-modules-umd β
β β ESM to UMD β
β β
β @babel/plugin-transform-modules-systemjs β
β β ESM to SystemJS β
β β
β INTEROP HELPERS: β
β __esModule marker β
β _interopRequireDefault β
β _interopRequireWildcard β
β β
β PURPOSE: β
β Write modern ESM β Babel transforms to target format β
β Ensures compatibility with older Node.js/browsers β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
9. Top-Level Await (ESM Feature)
Covered in detail above. Key points:
// config.js (ESM with Top-Level Await)
const response = await fetch('/api/config');
export const config = await response.json();
// database.js
export const db = await connectToDatabase();
// main.js
import { config } from './config.js'; // Waits for config to load
import { db } from './database.js'; // Waits for connection
// Both are guaranteed ready here
console.log(config.apiUrl);
await db.query('SELECT * FROM users');
Module Systems Comparison
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MODULE SYSTEMS COMPARISON β
ββββββββββββββββ¬ββββββββββ¬ββββββββββ¬ββββββββββ¬ββββββββββ¬ββββββββββ¬βββββββββββββ€
β Feature β IIFE β CJS β AMD β UMD β ESM β Bundler β
ββββββββββββββββΌββββββββββΌββββββββββΌββββββββββΌββββββββββΌββββββββββΌβββββββββββββ€
β Async Load β β β β β β
β β β β
β β
β
β Tree-shake β β β β β β β β β β
β β
β
β Live Binding β β β β β β β β β β
β β
β
β Static β β β β β β
β β β β
β β
β
β Browser β β
β β β β
β β
β β
β β
β
β Node.js β β
β β
β β β β
β β
β β
β
β Dynamic β β
β β
β β
β β
β β
* β β
β
β No Build β β
β β
β β
β β
β β
β β β
ββββββββββββββββΌββββββββββ΄ββββββββββ΄ββββββββββ΄ββββββββββ΄ββββββββββ΄βββββββββββββ€
β Status β Legacy β Legacy β Obsoleteβ Fading β Standardβ Modern β
ββββββββββββββββ΄βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
* ESM dynamic import via import()
Which Module System to Use?
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β DECISION GUIDE β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β NEW PROJECT (2024+): β
β ββββββββββββββββββββ β
β β Use ES Modules (ESM) everywhere β
β β Use a modern bundler (Vite, esbuild) β
β β Set "type": "module" in package.json β
β β
β LIBRARY/PACKAGE: β
β ββββββββββββββββ β
β β Publish dual CJS + ESM (exports field) β
β β Use Rollup or unbuild for bundling β
β β Include TypeScript types β
β β
β LEGACY PROJECT: β
β βββββββββββββββ β
β β Keep existing CommonJS if stable β
β β Migrate gradually to ESM β
β β Use Babel for interop if needed β
β β
β BROWSER-ONLY SCRIPT: β
β ββββββββββββββββββββ β
β β ESM with <script type="module"> β
β β Or Vite for development with HMR β
β β
β AVOID: β
β ββββββ β
β β AMD (obsolete) β
β β UMD (complex, declining) β
β β IIFE for anything beyond small scripts β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Key Takeaways
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ES MODULES SUMMARY β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β THREE PHASES: β
β 1. Construction: Parse and build dependency graph β
β 2. Instantiation: Link exports and imports (live bindings) β
β 3. Evaluation: Execute code depth-first, post-order β
β β
β LIVE BINDINGS: β
β β’ Imports are references to exports β
β β’ Changes in exporting module are visible to importers β
β β’ Read-only in importing module β
β β
β CIRCULAR DEPENDENCIES: β
β β’ Handled but may see undefined for uninitialized exports β
β β’ Use functions for deferred access β
β β
β CACHING: β
β β’ Each module evaluated once β
β β’ Same instance shared by all importers β
β β’ Natural singleton pattern β
β β
β BEST PRACTICES: β
β β
Use named exports for better tree-shaking β
β β
Keep module side effects minimal β
β β
Use dynamic import for code splitting β
β β
Avoid circular dependencies when possible β
β β
Use TLA sparingly (it blocks dependent modules) β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Next Steps
Continue to 22.5 Performance & Optimization Internals to learn about optimization killers, deoptimization triggers, and how to write highly performant JavaScript.