运行时包管理器打包器测试运行器代码检查格式化指南参考博客反馈

Workers

使用 Bun 的 Workers API 创建并与一个在独立线程上运行、但与主线程共享 I/O 资源的新 JavaScript 实例进行通信

借助 Worker,你可以启动一个在独立线程上运行的新 JavaScript 实例,并与其进行通信,同时与主线程共享 I/O 资源。

Bun 实现了 Web Workers API 的一个精简版本,并进行了扩展,使其更适用于服务器端场景。与 Bun 的其他部分一样,Worker 无需额外的构建步骤即可支持 CommonJS、ES 模块、TypeScript、JSX 和 TSX。

创建一个 Worker

与浏览器中一样,Worker 是全局对象。使用它可以创建一个新的工作线程。

在主线程中

index.ts
const worker = new Worker("./worker.ts");

worker.postMessage("hello");
worker.onmessage = event => {
  console.log(event.data);
};

工作线程

worker.ts
// 防止 TypeScript 报错
declare var self: Worker;

self.onmessage = (event: MessageEvent) => {
  console.log(event.data);
  postMessage("world");
};

为防止在使用 self 时出现 TypeScript 报错,请在工作线程文件顶部添加以下声明:

TypeScript
declare var self: Worker;

你可以在工作线程代码中使用 import 和 export 语法。与浏览器不同,使用 ES 模块时无需传递 {type: "module"}。

如果工作线程的脚本无法解析,Bun 会在 Worker 对象上触发一个 "error" 事件。

JavaScript
const worker = new Worker("/not-found.js");
worker.addEventListener("error", event => {
  console.log(event.message);
});

Bun 会相对于项目根目录解析传递给 Worker 的说明符(就像输入 bun ./path/to/file.js 一样)。

preload - 在 worker 启动前加载模块

将模块说明符数组传递给 preload 选项,以便在工作线程自身的代码运行前加载这些模块,其行为类似于 --preload CLI 参数。可以使用它来加载必须首先加载的代码,例如 OpenTelemetry、Sentry 或 DataDog。

index.ts
const worker = new Worker("./worker.ts", {
  preload: ["./load-sentry.js"],
});

你也可以将单个字符串传递给 preload 选项:

index.ts
const worker = new Worker("./worker.ts", {
  preload: "./load-sentry.js",
});

blob: URL

你也可以将 blob: URL 传递给 Worker,从字符串或其他内存中的源创建工作线程。

JavaScript
const blob = new Blob([`self.onmessage = (event: MessageEvent) => postMessage(event.data)`], {
  type: "application/typescript",
});
const url = URL.createObjectURL(blob);
const worker = new Worker(url);

与 Bun 的其他部分一样,从 blob: URL 创建的工作线程支持 TypeScript、JSX 和其他文件类型。要告知 Bun 源代码是 TypeScript,请在 Blob 上设置 type,或将 filename 传递给 File 构造函数。

TypeScript
const file = new File([`self.onmessage = (event: MessageEvent) => postMessage(event.data)`], "worker.ts");
const url = URL.createObjectURL(file);
const worker = new Worker(url);

"open" 事件

Bun 会在工作线程创建完成并准备好接收消息时触发 "open" 事件。(浏览器中不存在此事件。)

index.ts
const worker = new Worker(new URL("worker.ts", import.meta.url).href);

worker.addEventListener("open", () => {
  console.log("worker 已准备好");
});

在工作线程的脚本运行前发送的消息会进入队列,直到脚本运行后才会发送,因此你无需等待 "open" 事件再发送消息。这些消息会传递给脚本运行期间安装的处理器。如果脚本没有安装处理器,消息就会被丢弃。

与浏览器中一样,无论是否已存在 "message" 处理器,消息都会在工作线程脚本的同步部分运行后立即分发;如果分发消息时没有处理器,消息就会被丢弃。请在第一个顶层 await 之前安装 self.onmessage / self.addEventListener("message", ...):

worker.ts
declare var self: Worker;

// 在任何 `await` 之前安装:启动期间发送的消息不会遗漏
self.onmessage = (event: MessageEvent) => {
  console.log(event.data);
};

const config = await loadConfig();

静态导入的模块中的顶层 await 也同样适用。静态导入会在工作线程自身的代码运行前求值,因此,如果导入图中的任何模块在顶层使用了 await,消息就会在工作线程的第一行代码运行前开始分发,此时再在第一行安装处理器已经太晚了。请在安装处理器后使用动态 import() 加载这类模块,或使用 node:worker_threads。

node:worker_threads 在这方面与 Bun 和 Node.js 中的行为一致:parentPort 会将消息保留在队列中,直到附加 "message" 监听器(以及每次最后一个监听器被移除时),因此,在 parentPort.on("message", ...) 之前运行 await init() 的工作线程仍会按顺序接收所有消息。

使用 postMessage 发送消息

要发送消息,请使用 worker.postMessage 和 self.postMessage。Bun 使用 HTML Structured Clone Algorithm 序列化消息。

性能优化

Bun 针对常见数据类型的 postMessage 提供了快速路径:

字符串快速路径 - 发送纯字符串时,Bun 完全绕过结构化克隆算法,因此不会产生序列化开销。

简单对象快速路径 - 对于只包含原始值(字符串、数字、布尔值、null、undefined)的普通对象,Bun 会直接存储属性,而无需进行完整的结构化克隆。

简单对象快速路径适用条件:

  • 是普通对象且没有修改原型链
  • 只包含可枚举、可配置的数据属性
  • 没有索引属性或 getter/setter 方法
  • 所有属性值都是原始类型或字符串

有了这些快速路径,Bun 的 postMessage 性能可提升 2 到 241 倍,消息长度不再显著影响性能。

Bun(含快速路径):

TypeScript
postMessage({ prop: 11 字符串, ...9 个其他属性 }) - 648 纳秒
postMessage({ prop: 14 KB 字符串, ...9 个其他属性 }) - 719 纳秒
postMessage({ prop: 3 MB 字符串, ...9 个其他属性 }) - 1.26 微秒

Node.js v24.6.0(对比):

JavaScript
postMessage({ prop: 11 字符串, ...9 个其他属性 }) - 1.19 微秒
postMessage({ prop: 14 KB 字符串, ...9 个其他属性 }) - 2.69 微秒
postMessage({ prop: 3 MB 字符串, ...9 个其他属性 }) - 304 微秒
JavaScript
// 字符串快速路径 - 优化处理
postMessage("Hello, worker!");

// 简单对象快速路径 - 优化处理
postMessage({
  message: "Hello",
  count: 42,
  enabled: true,
  data: null,
});

// 复杂对象仍然可用,但会使用标准结构化克隆
postMessage({
  nested: { deep: { object: true } },
  date: new Date(),
  buffer: new ArrayBuffer(8),
});
JavaScript
// 在工作线程中,`postMessage` 自动“路由”至父线程。
postMessage({ hello: "world" });

// 在主线程中
worker.postMessage({ hello: "world" });

接收消息可使用 worker 或主线程的 message 事件处理器:

JavaScript
// 工作线程:
self.addEventListener("message", event => {
  console.log(event.data);
});
// 或者使用简写:
// self.onmessage = fn

// 如果在主线程:
worker.addEventListener("message", event => {
  console.log(event.data);
});
// 或者使用简写:
// worker.onmessage = fn

终止工作线程

Worker 实例会在其事件循环中没有剩余工作时自动终止。在全局对象或任何 MessagePort 上附加 "message" 监听器会使事件循环保持活动状态。要强制终止 Worker,请调用 worker.terminate()。

index.ts
const worker = new Worker(new URL("worker.ts", import.meta.url).href);

// ...一段时间后
worker.terminate();

调用 worker.terminate() 会使工作线程尽快退出。

process.exit()

工作线程可以通过 process.exit() 自行终止。这不会终止主进程。与 Node.js 中一样,process.on('beforeExit', callback) 和 process.on('exit', callback) 会在工作线程而非主线程上触发。Bun 会将退出代码传递给 "close" 事件。

"close" 事件

Bun 会在工作线程被标记为终止时触发 "close" 事件。工作线程完全退出可能需要一些时间。CloseEvent 包含传递给 process.exit() 的退出代码;如果因其他原因关闭,则为 0。

index.ts
const worker = new Worker(new URL("worker.ts", import.meta.url).href);

worker.addEventListener("close", event => {
  console.log("worker 正在关闭");
});

此事件在浏览器中不存在。

生命周期管理

默认情况下,活跃的 Worker 会保持主(创建它的)进程处于运行状态,因此 setTimeout 和 promise 等异步任务会使进程保持运行。附加 message 监听器也会使 Worker 保持运行。

worker.unref()

要阻止正在运行的 worker 使进程保持运行,请调用 worker.unref()。这会将 worker 的生命周期与主进程的生命周期解耦,其行为与 Node.js 的 worker_threads 一致。

index.ts
const worker = new Worker(new URL("worker.ts", import.meta.url).href);
worker.unref();

浏览器不支持 worker.unref()。

worker.ref()

要使进程保持运行,直到 Worker 终止,请调用 worker.ref()。默认情况下,Worker 会被引用;被引用的 worker 仍需要其事件循环中存在某些内容(例如 "message" 监听器)才能继续运行。

index.ts
const worker = new Worker(new URL("worker.ts", import.meta.url).href);
worker.unref();
// 以后...
worker.ref();

或者,也可以在传递给 Worker 的 options 对象中传入 ref: false,这与调用 worker.unref() 等效:

index.ts
const worker = new Worker(new URL("worker.ts", import.meta.url).href, {
  ref: false,
});

浏览器不支持 worker.ref()。

使用 smol 模式降低内存使用

Bun 的 Worker 支持 smol 模式,可以降低内存使用,但会牺牲性能。要启用该模式,请在 Worker 构造函数的 options 对象中传入 smol: true。

index.ts
const worker = new Worker("./i-am-smol.ts", {
  smol: true,
});
`smol` 模式究竟做了什么?

设置 smol: true 会将 JSC::HeapSize 设置为 Small,而非默认的 Large。

环境数据共享

使用 setEnvironmentData() 和 getEnvironmentData() 在主线程和工作线程间共享数据。

index.ts
import { setEnvironmentData, getEnvironmentData } from "worker_threads";

// 在主线程
setEnvironmentData("config", { apiUrl: "https://api.example.com" });

// 在工作线程
const config = getEnvironmentData("config");
console.log(config); // => { apiUrl: "https://api.example.com" }

Worker 事件

使用 process.on() 监听 worker 创建事件:

index.ts
process.on("worker", worker => {
  console.log("新 worker 已创建:", worker.threadId);
});

Bun.isMainThread

检查 Bun.isMainThread 以判断当前是否处于主线程。

TypeScript
if (Bun.isMainThread) {
  console.log("我是主线程");
} else {
  console.log("我在 worker 中");
}