執行環境套件管理器打包器測試執行器程式碼檢查格式化指南參考部落格意見回饋

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 中");
}