WebAssembly (Wasm) 是近年常被提到的話題:它讓 C、C++、Rust 等系統語言寫出來的程式碼能夠在瀏覽器上執行,補足 JavaScript 在高效能運算上的瓶頸。本章梳理 Wasm 的基本概念、使用方式,以及它與既有前端技術的關係。
直譯式語言 vs 編譯式語言#
JavaScript 屬於直譯式語言,執行時會逐行轉譯為機器碼,動態且彈性,但在效能上有天花板。C、C++ 等編譯式語言在執行前就已被編譯成機器碼,搭配靜態型別與底層控制權,效能通常可以推得更高。
那為什麼不直接在瀏覽器跑 C++?主要有兩個障礙:
- 編譯後的執行檔通常很大,傳輸成本太高。
- 若改成傳源碼再編譯,編譯本身又得花時間,使用者只會等更久。
WebAssembly 是什麼#
WebAssembly 是一種低階、類組合語言形式的二進位格式,已被列為 W3C 標準。它允許 C/C++/Rust 等語言以 Wasm 為編譯目標,再交由瀏覽器內建的 Wasm 編譯器解碼為機器碼後執行。簡單來說,這條路徑兼顧了二進位檔的壓縮性與接近原生的執行效能。
WebAssembly 並不是設計來取代 JavaScript,更像是與 JavaScript 並肩合作,補足 JS 不擅長的高耗能運算。
適合的場景#
- AR / VR
- 遊戲開發
- 數學運算、影像處理
- 加解密、區塊鏈
- IoT 與邊緣運算
.wasm 的長相#
直接寫 wasm 文字格式並不直觀,可讀性也不高:
(module
(import "console" "log" (func $log (param i32 i32)))
(import "js" "mem" (memory 1))
(data (i32.const 0) "Hi")
(func (export "writeHi")
i32.const 0
i32.const 2
call $log))實務上多數開發者會選擇用 C/C++/Rust 等高階語言撰寫,再透過 Emscripten 等工具編譯成 Wasm 模組。
對前端友善的選擇:AssemblyScript#
對 JS 開發者來說,從零開始學 Rust 或 C++ 確實有門檻。AssemblyScript 提供一條折衷路線:使用類似 TypeScript 的語法寫 Wasm。它支援 TypeScript 子集,最大的差別是型別必須使用 i32、u32 等明確的數值型別,而不是泛用的 number。
export function fib(n: i32): i32 {
let a = 0;
let b = 1;
if (n > 0) {
while (--n) {
const t = a + b;
a = b;
b = t;
}
return b;
}
return a;
}Hello World Demo#
初始化 AssemblyScript 專案的指令大致如下:
npm init -y
npm install --save @assemblyscript/loader
npm install --save-dev assemblyscript
npx asinit .asinit 會建立 assembly/(原始碼)、build/(編譯產物)、index.js(載入 Wasm 並輸出介面)等檔案。預設 assembly/index.ts 帶了一個 add 函式:
export function add(a: i32, b: i32): i32 {
return a + b;
}執行 npm run asbuild 後即可在 build/ 看到產生的 wasm 檔。
載入 Wasm 模組#
模組產生後,要在 JavaScript 環境中做 instantiation。常見方式有三種:
WebAssembly.Instance:同步初始化。WebAssembly.instantiate:非同步初始化。WebAssembly.instantiateStreaming:搭配fetch的串流式初始化。
WebAssembly.instantiateStreaming(fetch("/build/optimized.wasm"), {}).then(
(wasmModule) => {
const exports = wasmModule.instance.exports;
const memory = new Uint32Array(exports.memory.buffer);
}
);補充說明
instantiateStreaming在某些環境(例如 Node.js、Safari)支援度仍不完整,必要時可使用 polyfill。
用 IndexedDB 快取已編譯的模組#
每次重新下載與編譯 Wasm 模組是不必要的浪費。受限於 localStorage / sessionStorage 的大小限制,MDN 推薦使用 IndexedDB 來快取編譯後的模組。可以搭配 localForage 等套件簡化操作。
Wasm 與 Web Worker#
把 Wasm 模組放到 Web Worker 中執行,可以將 fetch、編譯、初始化的工作從 Main Thread 抽離,搭配運算密集的模組能顯著提升頁面流暢度。
但這個組合並非萬靈丹:
- Main Thread 與 Worker 之間的訊息傳遞會隨資料大小增加而變慢。
- 程式碼結構更複雜,模組互動變成非同步,需要額外處理 callback 或事件。
範例:在 React 中用 Wasm 跑 FFmpeg#
@ffmpeg/ffmpeg 將 FFmpeg 編譯為 Wasm,可在瀏覽器端做影片轉檔。傳統做法需要把檔案丟到後端再傳回,效率差且離線無法運作;Wasm 讓這些工作能搬到 client side。
npx create-react-app wasm-react-demo
npm i @ffmpeg/ffmpeg @ffmpeg/core值得注意的是 FFmpeg 的 Wasm 模組會使用 SharedArrayBuffer,需要設定 cross-origin isolated。它在內部維護一個 in-memory 檔案系統,讓 readFile、writeFile 等操作得以在瀏覽器端實作。
小結#
WebAssembly (Wasm) 並不會取代 JavaScript,更務實的觀點是「兩者互補」。它在某些情境(如影像處理、加解密、遊戲)能帶來顯著的效能提升,但同時也帶來除錯困難、檔案龐大等問題。對前端工程師而言,及早接觸 Wasm 與 AssemblyScript,能在面對高效能需求時多出一個選項。