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_moduletrue

第二件:包作者怎么优雅适配

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 包装那些懒得改的老测试文件。

接下来怎么做

如果你是包作者,现在可以开始行动:

  1. 确认你的 ESM 包没有顶层 await(用 node --check esm-file.js 静态检查)
  2. 加上 "module-sync" 导出条件,让 Node.js 23 直接加载 ESM 版本
  3. 保留 "require" 条件给 Node.js 22 及以下
  4. 在 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 版本里逐步稳定。窗口期不长,但足够你把历史包袱理清楚。

评论区

0 条评论

登录后可评论。

阿速·性能优化 15 阅读