npm 包写了八年,每次 require 一个 ESM 都要换 import()——今天 Node.js 23 把这件事彻底原生化了
写过 npm 包的人都踩过这个坑——Node.js 生态里 CJS 和 ESM 混在一起是常态,每次要动态加载一个 ES 模块,都要写一整块 import(),还要包 await,拿到的还是命名空间对象而不是直接可用的 module.exports。
这个写法从 2017 年说到现在,没人觉得有问题。直到 Node.js 23 默认把 require(esm) 启用了。
到底发生了什么
Node.js 23.0.0(2026 年 9 月发布)把 --experimental-require-module 这个实验性 flag 变成了默认行为。也就是说,现在在一个 CommonJS 模块里直接 require('./esm-module') 加载原生 ES 模块,Node.js 不再抛 ERR_REQUIRE_ESM,而是直接返回 ES 模块的命名空间对象——跟 import() 的行为一致。
// app.cjs (CommonJS)
const { default: foo, bar } = require('./esm-module.js');
console.log(foo, bar);
以前等价写法:
// app.cjs
import('./esm-module.js').then(({ default: foo, bar }) => {
console.log(foo, bar);
});
Node.js 22 及之前,这个功能需要 --experimental-require-module 才能用。Node.js 23 默认打开,首次遇到 ESM 时会发一个实验性警告,但不会阻断执行。
三件必须知道的事
第一件:如何检测当前环境支不支持
if (process.features.require_module) {
console.log('支持 require(esm)');
}
Node.js 23 支持,process.features.require_module 为 true。
第二件:包作者怎么优雅适配
ESM 包可以声明 "module-sync" 导出条件,用来检测当前环境是否支持同步 require:
{
"exports": {
".": {
"import": "./dist/esm/index.js",
"module-sync": "./dist/esm/index.js",
"require": "./dist/cjs/index.js"
}
}
}
这样 require() 和 import 在 Node.js 23 下会加载同一个文件,不再需要维护两套产物。
第三件:顶层 await 可能让你的代码崩掉
如果被 require 的 ES 模块或其依赖有顶层 await,Node.js 会抛 ERR_REQUIRE_ASYNC_MODULE。这不是 bug,是规范要求——同步 require 没法等待异步初始化。
// esm-dep.js
const data = await fetch('/api/config'); // 顶层 await
export default data;
// cjs-loader.js
try {
const config = require('./esm-dep.js'); // ERR_REQUIRE_ASYNC_MODULE
} catch (e) {
console.error(e.code); // ERR_REQUIRE_ASYNC_MODULE
}
如果你的包依赖有顶层 await,别急着开这个功能。
性能这件事到底变了多少
从异步 import() 到同步 require(),模块加载本身的时间没有本质变化(都是 V8 在解析),但有几个实际差异:
- 不需要 Promise 包装:代码从 3 层缩到 1 层,执行栈浅一层
- 不需要动态 import:静态分析工具更容易读懂依赖图
- CommonJS 的
module.exports =和 ESM 的export default混用场景更自然
对于 CI 构建场景,require() 可以直接串进同步流程,不需要 Promise.all() + await 包装那些懒得改的老测试文件。
接下来怎么做
如果你是包作者,现在可以开始行动:
- 确认你的 ESM 包没有顶层 await(用
node --check esm-file.js静态检查) - 加上
"module-sync"导出条件,让 Node.js 23 直接加载 ESM 版本 - 保留
"require"条件给 Node.js 22 及以下 - 在 CI 里跑 Node.js 23 + Node.js 22 两套测试,确保都过
如果你是应用开发者,看你的依赖链里有没有这类混用场景。有的话先在本地 Node.js 23 跑一遍,看有没有 ERR_REQUIRE_ASYNC_MODULE 警告——有就说明某个 ESM 依赖还没准备好。
Node.js 22 将在 2026 年 10 月进入 LTS,Node.js 23 的这些实验性调整会在之后的 LTS 版本里逐步稳定。窗口期不长,但足够你把历史包袱理清楚。
评论区
登录后可评论。