Node 26.9.0 把 Web Worker 原生接进来了——一个 flag,CPU 密集任务不用再装 workerpool

如果你的 Node 服务里有一段 CPU 密集逻辑——图片转码、大 JSON 解析、加密校验、报表计算——你一定经历过这件事:它一跑,整个事件循环就卡住,所有 HTTP 请求排队等它。你翻遍方案,最后要么装 workerpool / piscina 这类库,要么自己手搓 node:worker_threads,还得写一堆消息协议和线程池管理。

Node.js 26.9.0(2026 年 9 月 16 日发布)里有一条不太起眼的提交,恰好把这件事推到台前:worker: add support for Web Workers(PR #64894,作者 Aviv Keller,修的是 2021 年就开的 issue #43583)。简单说,Node 终于内建了一个浏览器兼容版的 Web Worker API,你写惯的那套 new Worker + postMessage + addEventListener 可以直接搬进 Node。

这轮先给结论:它能用,但别急着把手上的 workerpool 拆了。 它现在是实验特性,要加 flag,而且和 node:worker_threads 相比特意砍掉了一批能力。看完你再决定哪些场景迁移、哪些继续留在旧 API。

这波到底补了什么

过去 Node 里做并行只有 node:worker_threads,它是给服务端设计的:构造 worker 要传文件路径或 eval,通信靠 parentPort,事件名是 message/error/exit。浏览器那边则是 Web Worker:全局是 self,有 name、location、navigator,用 postMessage() / close() / importScripts()。两套心智模型,前端转后端就得重学一遍。

26.9.0 直接把 Web Worker 这层 API 铺在了 node:worker_threads 之上。文档写得很直白:「A mostly browser-compatible implementation of Web Workers of the HTML Standard, implemented on top of node:worker_threads」。

启用方式就是加一个 CLI flag:

node --experimental-web-worker main.js

然后就是你在浏览器里写熟了的代码:

// worker.js
addEventListener('message', (event) => {
  postMessage(`${event.data} from ${name}!`);
});
// main.js
const worker = new Worker('./worker.js', { name: 'greeter' });

worker.addEventListener('message', (event) => {
  console.log(event.data); // Prints: Hello from greeter!
  worker.terminate();
});

worker.postMessage('Hello');

注意几个细节,都是文档里明确写的:

  • worker 全局对象上除了 self、name、location、navigator,还保留了 process、Buffer、require() 这些 Node 全局——它不是纯浏览器沙箱,而是把 DedicatedWorkerGlobalScope 插进了 Node 全局对象原型链。
  • SharedWorker 没有实现。文档的理由是它的生命周期和共享模型依赖 origin 和 browsing context,Node 里没有这套东西。
  • 脚本加载走本地文件系统或内存,不走网络。 new Worker() 和 importScripts() 只认 file:、data:、blob: 三种 URL,其他 scheme 直接抛 NotSupportedError / NetworkError。重定向、nosniff、HTTP MIME 校验这些浏览器里的坑,Node 里不存在。
  • 主线程里相对路径按当前工作目录解析,因为没有 document base URL。

想迁移之前,先看这份「砍掉了什么」清单

这是全文最该记住的一段。Node 官方文档专门列了一节 Differences from the HTML Standard,挑几个会真正坑到你的:

  1. 没有 origin 模型。 不存在同源/跨源,location.origin 永远是 'null'。别指望用 Web Worker 做任何来源隔离。
  2. close() 是立即终止,不是规范的「closing flag」算法。 也就是 close() 之后当前任务里剩下的代码不会执行。浏览器里会跑完当前 task,Node 里不会——这个差异能让你踩出一个偶现 bug。
  3. 未捕获异常直接杀掉 worker 线程,而且未处理的 error 事件不会向上冒泡,既不影响父级全局作用域,也不影响进程退出码。也就是说,worker 里的异常如果不显式处理,你会静默丢任务。
  4. ErrorEvent 的 filename / lineno / colno 恒为空值(''、0、0),定位报错全靠自己加日志。
  5. 这几个事件永远不会触发:languagechange、online、offline,以及 rejectionhandled / unhandledrejection(Node 没实现 PromiseRejectionEvent)。

还有一个很容易忽略的:Web Worker 默认会让事件循环保持存活,和 worker_threads 一样。要想让进程能正常退出,得用 process.unref(worker)(Web Worker 实现了 Refable 协议,配 process.ref() / process.unref() 用)。这一点你从浏览器搬代码时最容易忘——浏览器里 worker 不会阻止页面卸载,Node 里会阻止进程退出。

那什么时候该用它,什么时候继续用 worker_threads

文档给了个判定口诀,我翻译成人话:

用 node:worker_threads,如果你需要:workerData、自定义 env / execArgv、资源上限(resourceLimits)、stdio 重定向、'online' / 'exit' 事件,或者 worker.threadId。

用新的 Web Worker,如果你想:让同一份 worker 代码在浏览器和 Node 里跑同一套写法,或者你就是来复用前端已有的 worker 逻辑、不想写一层适配。

翻译成决策:

  • 你在写同构代码(同一份 worker 要在浏览器和 Node 两端跑)→ 新 API 是真香,省掉一层 polyfill 和消息协议胶水。
  • 你在写纯服务端线程池(要控并发、要资源隔离、要调 native 参数)→ 继续 worker_threads / piscina,新 API 反而少了一半你要的能力。

顺带提醒:Web Worker 底层就是 node:worker_threads,二者共享线程、结构化克隆、transferable 语义。所以你现在的 postMessage / ArrayBuffer transfer 知识完全可迁移,不是学了套新东西。

给你的下一步

  1. 先别上生产。它是 --experimental-web-worker 下的实验特性,Node 26 还在 Current 阶段(要到 2026 年 10 月才进 LTS)。拿个测试分支试。
  2. 挑一个纯计算、无 I/O、输入输出都能结构化克隆的模块作为第一个试点,比如哈希校验或数据聚合。
  3. 试的时候重点验三件事:close() 之后剩下的同步代码有没有执行、worker 里抛异常会不会丢掉父进程的回调、进程能不能正常退出(记得 process.unref)。
  4. 如果这三条都过了,再考虑把同构场景的 worker 代码统一到 Web Worker 写法;服务端线程池维持原样。

一句话收尾:Node 补齐 Web Worker,价值不在「又多一个并行 API」,而在于前后端 worker 代码终于可以只写一份。至于性能收益,它和 worker_threads 是同一套底层,别指望换写法就能更快——真正省下来的是你那层适配代码和心智负担。

评论区

0 条评论

登录后可评论。

阿速·性能优化 139 阅读