Transformers.jsViteWebGPUWASMTypeScript

Transformers.js 4.0 + Vite 8 升級風險:前端 AI fallback 實戰

整理 Transformers.js 4.0 在 Vite 8 的型別、Worker、WASM 路徑風險,附 WebGPU→WASM fallback 與上線檢查清單。

· 6 分鐘閱讀

這篇文章會帶你把 Transformers.js 4.0 + Vite 8 的升級風險,落成可上線的 fallback 實作與檢查清單。


前言

前端團隊導入瀏覽器端 AI 時,最容易低估的不是模型效果,而是升級風險:套件版本對了,型別卻壞掉;本機跑得動,上線後 WASM 路徑 404;某些裝置 WebGPU 初始化失敗,整個功能直接掛掉。

這篇不是泛用 AI 入門,而是聚焦你在升級到 @huggingface/transformers@4.0.0vite@8.0.3(截至 2026-04-02)時,最常踩到的三個坑:TypeScript 型別、推論執行緒隔離、部署資產路徑。

目標是讓你今天就能把「WebGPU 優先、WASM 保底」這套流程落地,並保留可回滾空間。

核心主題 1:4.0 升級時先處理型別與初始化邊界

這次最常見的破壞點,是把舊範例直接搬到 4.0。實務上至少要先檢查兩件事:

  1. 不要假設存在通用 Pipeline 型別;改用實際 task 對應的型別,例如 TextClassificationPipeline
  2. fallback 不要只靠 navigator.gpu 判斷,因為「有 API」不代表 runtime 初始化一定成功。

下面是可直接放在 Worker 的最小可執行範例。

// src/ai/worker.ts
import {
  env,
  LogLevel,
  pipeline,
  type TextClassificationPipeline,
} from '@huggingface/transformers'

type InferRequest = { id: string; text: string }
type InferResponse = {
  id: string
  ok: boolean
  backend: 'webgpu' | 'wasm'
  output?: unknown
  error?: string
}

const MODEL_ID = 'Xenova/distilbert-base-uncased-finetuned-sst-2-english'
let classifierPromise: Promise<TextClassificationPipeline> | null = null
let backend: 'webgpu' | 'wasm' = 'wasm'

env.logLevel = LogLevel.WARNING

async function createClassifier(): Promise<TextClassificationPipeline> {
  try {
    const webgpuClassifier = await pipeline('sentiment-analysis', MODEL_ID, {
      device: 'webgpu',
      dtype: 'fp32',
    })
    backend = 'webgpu'
    return webgpuClassifier as TextClassificationPipeline
  } catch {
    // 中文註解:WebGPU 失敗時,改走預設 WASM runtime
    const wasmClassifier = await pipeline('sentiment-analysis', MODEL_ID, {
      dtype: 'q8',
    })
    backend = 'wasm'
    return wasmClassifier as TextClassificationPipeline
  }
}

function getClassifier() {
  if (!classifierPromise) classifierPromise = createClassifier()
  return classifierPromise
}

self.onmessage = async (event: MessageEvent<InferRequest>) => {
  const { id, text } = event.data
  try {
    const classifier = await getClassifier()
    const output = await classifier(text)
    const response: InferResponse = { id, ok: true, backend, output }
    self.postMessage(response)
  } catch (error) {
    const response: InferResponse = {
      id,
      ok: false,
      backend,
      error: error instanceof Error ? error.message : 'unknown error',
    }
    self.postMessage(response)
  }
}

核心主題 2:Vite 8 要把推論放進 Worker,避免 UI 掉幀

Vite 8 本身不會幫你解決主執行緒卡頓。若推論直接跑在 UI thread,首載模型時很容易造成互動延遲。最穩的方式是固定用 Worker + 訊息協定,讓 UI 只負責發 request 與顯示結果。

// src/ai/client.ts
const worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' })

export async function classify(text: string) {
  const id = crypto.randomUUID()

  return new Promise<{ label: string; score: number; backend: 'webgpu' | 'wasm' }>(
    (resolve, reject) => {
      const onMessage = (event: MessageEvent) => {
        const data = event.data
        if (!data || data.id !== id) return

        worker.removeEventListener('message', onMessage)

        if (!data.ok) {
          reject(new Error(data.error ?? 'inference failed'))
          return
        }

        // 中文註解:sentiment-analysis 預設回傳陣列,取第一筆即可
        const first = Array.isArray(data.output) ? data.output[0] : data.output
        resolve({ label: first.label, score: first.score, backend: data.backend })
      }

      worker.addEventListener('message', onMessage)
      worker.postMessage({ id, text })
    },
  )
}

工程上建議加一條規則:同一頁面若連續失敗達門檻(例如 3 次),直接關閉端側推論並切回伺服器 API,避免使用者一直重試。

核心主題 3:wasmPaths 是部署設定,不是每個專案都要硬改

許多文章會直接寫 env.backends.onnx.wasm.wasmPaths = '/wasm/',但在 TypeScript 專案常遇到型別不穩。更重要的是,這個設定本質上是「資產部署路徑」問題,只有在你要自管 ONNX Runtime WASM 檔案時才需要設定。

// src/ai/runtime.ts
import { env } from '@huggingface/transformers'

type OnnxEnvWithWasm = typeof env.backends.onnx & {
  wasm?: {
    wasmPaths?: string | Record<string, string>
  }
}

const onnxEnv = env.backends.onnx as OnnxEnvWithWasm

// 中文註解:只有你要把 wasm 檔放在 /public/wasm 或 CDN 時才需要覆寫
if (onnxEnv.wasm) {
  onnxEnv.wasm.wasmPaths = '/wasm/'
}

env.useWasmCache = true

搭配 Vite 8,建議你把部署策略固定成以下其中一種:

  1. 交給套件預設下載(簡單,但首次載入較慢)。
  2. 自家 CDN 託管 WASM(可控性高,需維護版本映射)。
  3. 站內 /public/wasm 託管(最直覺,需確認 cache header 與路徑一致)。

核心主題 4:把升級風險寫成可驗證清單

如果你的團隊要在 Sprint 內交付功能,建議不要只靠「能跑起來」做驗收,而是把風險拆成可以打勾的項目。下面這份清單,是我在前端專案導入端側推論時最常用的最小版本。

4.0 / Vite 8 風險對照

  1. 型別風險: Pipeline 泛型假設過度,導致 CI 在 tsc --noEmit 才爆。解法是改用 task 專屬型別,或在邊界層做明確 as 轉型並集中管理。
  2. 執行期風險: 裝置宣告有 WebGPU,但 runtime 初始化失敗。解法是 Worker 內 fallback,不把判斷放在 UI 層就結束。
  3. 資產風險: WASM 路徑在 dev 正常、部署後 404。解法是把 wasmPaths 與 CDN/public 路徑納入 deploy checklist,並在 staging 先驗證。
  4. 觀測風險: 推論失敗只在 console 出現,沒有監控事件。解法是上報 backend、model id、error type、裝置資訊,才能做事故切分。

建議的 CI / QA 驗證順序

  1. TypeScript gate:每次 PR 必跑 tsc --noEmit,先擋掉型別退化。
  2. fallback 測試:模擬 WebGPU 不可用,確認仍能回傳可用結果。
  3. 資產 smoke test:用 staging 網域實際載入 WASM,確認路徑與快取標頭。
  4. 壓力測試:同時發 10-20 次請求,確認 Worker 不會造成主執行緒長時間卡頓。
  5. 回滾演練:透過 feature flag 關閉端側推論,驗證 API fallback 可即時生效。

這段流程的重點只有一個:你要有能力在「版本升級」和「產品穩定」之間快速切換,而不是每次都靠人工猜測。

分階段遷移路線(建議 2 週)

  1. 第 1-2 天:先把 Worker 與 fallback 跑通,只接一個低風險任務(例如 sentiment 或關鍵字分類)。
  2. 第 3-5 天:補齊監控欄位與錯誤分級,至少能區分「模型下載失敗」與「推論失敗」。
  3. 第 6-8 天:在 staging 做裝置分層驗證,蒐集桌機/中階手機/低階手機的延遲差異。
  4. 第 9-10 天:灰度 5%-10% 流量,觀察錯誤率、P95 latency、回滾頻率,再決定是否擴大。

很多團隊導入失敗,不是技術做不到,而是直接一步到位推全流量。端側推論應該像基礎設施升級一樣,先小範圍、可觀測、可回退,再逐步放大。

常見問題 / 注意事項

WebGPU 可用就一定要強制走 WebGPU 嗎?

不建議強制。某些裝置初始化成本很高,短文本任務不一定比 WASM 快。建議用真實流量觀察 P95 latency 再決定預設路徑。

Vite 8 升級時,最優先該補哪種測試?

先補「初始化 + fallback」整合測試。你要驗證的是:WebGPU 失敗時,功能仍能在 WASM 下回傳結果。

什麼情況不建議現在導入端側推論?

如果你沒有錯誤監控、沒有回滾開關、也沒有裝置分層策略,先不要上線。先把觀測與 fallback 打好,風險會小很多。

總結

Transformers.js 4.0 搭配 Vite 8 已經能做出可上線的瀏覽器端推論,但前提是你把升級風險當成工程問題來處理,不是只看 demo 能不能跑。

這篇的落地順序可以這樣做:

  1. 先修正型別與 Worker 初始化,確保最小可執行。
  2. 補上 WebGPU 失敗自動 fallback 到 WASM。
  3. 依部署策略決定是否設定 wasmPaths,並加上回滾開關。

把這三步做好,你的端側 AI 功能才不會在真實裝置上變成隨機故障源。