到目前為止,我們所有的影像處理都在瀏覽器 主執行緒 上跑。這代表 UI、事件、排版、繪圖,還有那堆重計算,全都擠在同一條線。結果就是:一旦你對一張 4K 圖片做卷積或模糊,畫面立刻卡死,滑桿拖不動,按鈕沒反應。
這正是 Web Worker 要解決的問題。Worker 是瀏覽器提供的「背景工人」:另一個 JS 執行環境,不能直接碰 DOM,但可以專心算東西,最後透過 postMessage
把結果傳回來。主執行緒保持流暢,專心處理 UI 與互動。
可以這樣想:
今天先做最簡單的 單工版 Worker:一次只處理一個任務,跑完再接下一個:
{ type: 'init' }
:Worker 初始化 WASM。{ type: 'run', w, h, ops, bytes }
:Worker 執行 apply_pipeline
,把結果帶回。新建 demo/src/worker.ts
,專門處理初始化和執行。程式碼大意如下:
// demo/src/worker.ts
/// <reference lib="webworker" />
import init, { apply_pipeline } from 'rustwasm-test'
import wasmUrl from 'rustwasm-test/rustwasm_test_bg.wasm?url'
let ready = false
self.addEventListener('message', async (ev: MessageEvent) => {
const msg = ev.data
if (msg.type === 'init') {
try {
await init(wasmUrl)
ready = true
;(self as any).postMessage({ ok: true })
} catch (e: any) {
postError(e)
}
return
}
if (msg.type === 'run') {
if (!ready) return postError({ code: 'NOT_READY', message: 'WASM not initialized' })
try {
const out = apply_pipeline(msg.bytes, msg.w, msg.h, msg.ops) as Uint8Array
;(self as any).postMessage({ ok: true, bytes: out }, [out.buffer]) // transfer buffer
} catch (e: any) {
postError(e)
}
}
})
function postError(e: any) {
const err = (e && typeof e === 'object' && 'code' in e && 'message' in e)
? e
: { code: 'INTERNAL', message: String(e?.message ?? e) }
;(self as any).postMessage({ ok: false, error: err })
}
export {}
在 main.ts
裡,我們把 apply_pipeline
的直接呼叫改成透過 Worker。只需要新增 Worker 的初始化與代理函式:
// demo/src/main.ts
const worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' })
// 初始化
await new Promise<void>((resolve, reject) => {
const onMsg = (ev: MessageEvent<any>) => {
worker.removeEventListener('message', onMsg)
if (ev.data?.ok) resolve()
else reject(ev.data?.error)
}
worker.addEventListener('message', onMsg)
worker.postMessage({ type: 'init' })
})
// 把 runPipeline 改成丟給 worker
async function runPipeline(ops: unknown[]) {
if (!w || !h) return
const img = ctx.getImageData(0, 0, w, h)
const input = new Uint8Array(img.data.buffer)
try {
const out = await runInWorker(input, w, h, ops)
img.data.set(out)
ctx.putImageData(img, 0, 0)
} catch (e) {
showWasmError(e)
}
}
function runInWorker(bytes: Uint8Array, w: number, h: number, ops: unknown[]) {
return new Promise<Uint8Array>((resolve, reject) => {
const onMsg = (ev: MessageEvent<any>) => {
worker.removeEventListener('message', onMsg)
const data = ev.data
if (data?.ok) resolve(data.bytes as Uint8Array)
else reject(data?.error)
}
worker.addEventListener('message', onMsg)
worker.postMessage({ type: 'run', w, h, ops, bytes })
})
}
其他部分(載圖、滑桿、按鈕)都不用動。