iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
Rust

把前端加速到天花板:Rust+WASM 即插即用外掛系列 第 13

Day 13|請你們家勞公出來喔~(Web Worker

  • 分享至 

  • xImage
  •  

到目前為止,我們所有的影像處理都在瀏覽器 主執行緒 上跑。這代表 UI、事件、排版、繪圖,還有那堆重計算,全都擠在同一條線。結果就是:一旦你對一張 4K 圖片做卷積或模糊,畫面立刻卡死,滑桿拖不動,按鈕沒反應。

這正是 Web Worker 要解決的問題。Worker 是瀏覽器提供的「背景工人」:另一個 JS 執行環境,不能直接碰 DOM,但可以專心算東西,最後透過 postMessage 把結果傳回來。主執行緒保持流暢,專心處理 UI 與互動。

可以這樣想:

  • 主執行緒:顯示畫面、處理使用者輸入。
  • Worker 執行緒:接收任務 → 執行影像管線 → 回傳結果。

今天先做最簡單的 單工版 Worker:一次只處理一個任務,跑完再接下一個:

  • { type: 'init' }:Worker 初始化 WASM。
  • { type: 'run', w, h, ops, bytes }:Worker 執行 apply_pipeline,把結果帶回。

Worker 端(worker.ts)

新建 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

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 })
  })
}

其他部分(載圖、滑桿、按鈕)都不用動。


上一篇
Day 11|Result 的錯誤物語:達達的馬蹄與 {code, message}
下一篇
Day 13|人多力量大! Web Worker 多工 + 佇列
系列文
把前端加速到天花板:Rust+WASM 即插即用外掛18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言