Understanding the ESModule Specification and Its Implementation in Node.js and Browsers
This article explains the ESModule standard, compares it with CommonJS and AMD, describes how Node.js and modern browsers implement ESModules, discusses compatibility challenges with third‑party dependencies, and reviews next‑generation tools such as Snowpack and Vite that leverage native module loading.
1 ESModule Specification
Introduction
Before ES6, the community created module loading solutions like CommonJS (for servers) and AMD (for browsers). ES6 introduced a native module system that satisfies most needs of both. With ESModule, browsers and Node.js can natively support a unified module format.
After ES6 defined ESModule, browsers began to support it natively, and Node.js added support as well.
// commonjs
const axios = require('axios')
// amd
require(['axios'], function (axios) {})
// esmodule
import axios from './node_modules/axios/axios.js'Note: The latest ECMAScript proposal is approved; import maps can manage module import paths.
Server‑side Implementation
CommonJS
Node.js originally used its own built‑in Module system to implement CommonJS, which is not an official JavaScript standard and is unsupported by browsers.
ESModule
Node.js needed to adopt ESModule while preserving existing CommonJS packages, so the solution was to support both formats.
Compatibility Issues
Because CommonJS and ESModule have fundamental design conflicts, compatibility layers are complex.
Node.js >= v13 uses the .mjs extension for ESModules and the "type": "module" field in package.json to treat .js files as ESModules.
// esmA/index.mjs
export default null
// esmB/index.js
export default null
// esmB/package.json
{
"type": "module"
}Browser Implementation
Modern Browser Support
All major browsers (except IE) have implemented ESModule support within the last few years.
Example repository: https://github.com/mdn/js-examples/tree/master/modules/basic-modules
2 Third‑Party Dependency Dilemma
Node.js Module Resolution
Node.js resolves third‑party packages by name, automatically concatenating node_modules paths and reading the entry field in package.json .
const axios = require('axios') // => resolves to node_modules/axios
const axios = require('/User/xxxx/workspace/node_modules/axios') // => resolves package.json
const axios = require('/User/xxxx/workspace/node_modules/axios/axios.js') // => resolves entry fileAuto‑append full path to node_modules
Auto‑read entry field in package.json
Auto‑add file extensions or index paths
Webpack Module Resolution
Webpack bundles all dependencies into a single bundle.js . It mimics Node.js CommonJS handling so developers can use the same syntax in web apps.
However, Webpack’s internal CommonJS implementation differs from Node.js’s core implementation.
Browser Module Resolution
Browsers do not support native CommonJS loading; even with ESModule support, they cannot directly load CommonJS packages, prompting the need for new tooling.
3 Next‑Generation Web App Development Model
To avoid the incompatibility of CommonJS packages with browsers, tools convert CommonJS modules to ESModules.
CommonJS to ESModule Converters
JSPM (https://jspm.dev/@babel/core)
Skypack (http://cdn.skypack.dev/@babel/core)
ESM.sh (http://esm.sh/@babel/core)
These tools use bundlers like Rollup or esbuild to transform syntax and rewrite import paths.
const axios = require('axios')
module.exports = axios
// =>
import axios from '/esm/axios.js'
export default axiosIn the browser, the transformed module can be loaded directly:
Using this approach can cause a “request explosion” when many third‑party modules are needed (e.g., an Ant Design component library may trigger ~2000 network requests).
Snowpack & Vite
These tools separate third‑party dependencies from source code, bundling dependencies once into a “dep chunk” while letting the browser handle native ESModule loading for source files, dramatically improving dev‑server startup time.
4 Adoption Challenges
Although the new model reduces tooling complexity and speeds up startup, ecosystem adoption is slow due to incomplete support, conversion gaps, and dynamic‑import issues.
Ecology Issues
Many features already available in Webpack lack official support in newer tools.
CommonJS‑to‑ESModule Conversion Problems
Conversion tools often fail to handle complex CommonJS patterns, dynamic exports, and legacy compatibility code (e.g., exports.__esModule ).
Dynamic Import vs. Static Analysis
CommonJS’s dynamic require makes static analysis difficult, whereas ESModule requires top‑level static exports.
// CommonJS dynamic example
init()
function init() { exports.a = 1 }
// ESModule static example
export const a = 1
// Error if export is inside a function
init()
function init() { export const a = 1 }Named vs. Default Exports
CommonJS treats module.exports and exports as the same object, while ESModule distinguishes named and default exports.
// module.js (ESModule)
export const a = 1
export default { a: 2 }
// index.js
import { a } from './module.js' // 1
import aDefault from './module.js' // { a: 2 }
import * as all from './module.js' // { a: 1, default: { a: 2 } }Proposed Solutions
The ByteDance Web Infra team is actively working on ESModule adoption, addressing the above challenges, and will share detailed solutions in future posts.
Follow the ByteDance Web Infra public account for updates.
政采云技术
ZCY Technology Team (Zero), based in Hangzhou, is a growth-oriented team passionate about technology and craftsmanship. With around 500 members, we are building comprehensive engineering, project management, and talent development systems. We are committed to innovation and creating a cloud service ecosystem for government and enterprise procurement. We look forward to your joining us.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.