Why ES6 Modules in Node.js Are Harder Than You Think – An Inside Look
James M. Snell explains the fundamental differences between CommonJS and ES6 modules in Node.js, why loading and execution timing matter, the challenges of supporting both systems, and the roadmap for native ES6 module support.
Understanding When You Need to Know What You Need
James M. Snell, IBM Technical Lead for Node.js, previously described the many differences between the existing CommonJS module system and the new ES6 module system, and now provides an update on the progress of implementing ES6 modules in the Node.js core.
Timing Is Crucial
In CommonJS, a module’s structure (its API) is unknown until the module’s code has been executed; even after execution the structure can be altered at runtime. By contrast, ES6 modules expose their exported bindings during the parsing phase, before any code runs, allowing the loader to know the module’s shape ahead of execution.
Example of a simple CommonJS module:
function foo() {
return 'bar';
}
function bar() {
return 'foo';
}
module.exports.foo = foo;
module.exports.bar = bar;And its usage in app.js:
const {foo, bar} = require('foobar');
console.log(foo(), bar());When node app.js runs, Node loads app.js, encounters the synchronous require(), loads foobar.js synchronously, parses and executes it, and then returns the exported object. All of this happens within a single event‑loop tick.
Now the same module written with ES6 syntax:
export function foo() {
return 'bar';
}
export function bar() {
return 'foo';
}And its import:
import {foo, bar} from 'foobar';
console.log(foo());
console.log(bar());Loading an ES6 module starts by reading the file (potentially asynchronously), then parsing it. During parsing, the exported bindings are discovered before any code runs. The import and export statements are resolved before execution, and the actual execution may be spread across multiple event‑loop turns.
Why the Difference Matters
Because require() is a fully synchronous function, it cannot be used to load an ES6 module whose loading, parsing, and execution are asynchronous. Changing require() to be asynchronous would break the existing ecosystem. One proposal is to add require.import(), which would return a Promise that resolves when the ES6 module finishes loading.
Conversely, ES6 modules can import CommonJS modules without asynchronous loading, as the spec allows a CommonJS module to be treated as a default export.
Named Imports
ES6 modules support named imports that are resolved during the parsing phase, e.g.: import {foo, bar} from 'foobar'; In CommonJS, the module’s shape is unknown until execution, so named imports cannot be statically resolved without a major change to the ECMAScript spec. Instead, a CommonJS module is typically imported as a default object:
import foobar from 'foobar';
console.log(foobar.foo(), foobar.bar());Attempting to use the ES6 named‑import syntax with a CommonJS module ( import {foo, bar} from 'foobar';) will fail because the bindings foo and bar are not directly exported.
Babel’s Role
Tools like Babel transpile ES6 module syntax into CommonJS so that the code can run in current Node.js versions. However, Babel’s transformed code does not preserve the true semantics of native ES6 named imports, which are resolved before execution.
“Michael Jackson Script” – The .mjs Extension
Node.js needs a way to know whether a file should be treated as a CommonJS script or an ES6 module. After exploring many options, the team settled on the *.mjs file extension to explicitly mark ES6 modules. This convention is sometimes humorously called the “Michael Jackson Script”.
Roadmap
At the time of writing, significant work remains on both the specification side and the V8 engine side before Node.js can fully support ES6 modules. The team estimates that a stable implementation will take roughly a year.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Aotu Lab
Aotu Lab, founded in October 2015, is a front-end engineering team serving multi-platform products. The articles in this public account are intended to share and discuss technology, reflecting only the personal views of Aotu Lab members and not the official stance of JD.com Technology.
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.
