2016 WebAssembly:JavaScript 效能天花板之謎#
案發現場#
2013 年,Mozilla 的 Alon Zakai 把一個 C++ 遊戲引擎編譯成 JavaScript(asm.js 專案),在瀏覽器裡跑出了接近原生的效能——但這是硬撐出來的。同一時期,Unity 的開發者抱怨 C# 遊戲編譯到瀏覽器後效能掉了一半以上,AutoCAD、Photoshop 的團隊則根本不敢把核心搬上瀏覽器。
當時的未解之謎:
- JavaScript 的效能天花板。 JS 引擎(V8、SpiderMonkey)的 JIT 編譯已經做到極致,但語言本身的動態性是硬傷:型別不確定(
a + b可能是數字相加、字串串接或 NaN)、物件形狀多變(hidden class 退化導致去最佳化)、沒有 64 位元整數、GC 停頓不可控。數值運算與遊戲迴圈的天花板約為原生效能的 50–70%,而且極不穩定。 JIT 的推導瓶頸:引擎必須先「猜」型別、再驗證、猜錯就回退——這個回退(deopt)機制的成本無法根除。 - 沒有可攜的二進制格式。 瀏覽器只有一種「程式」:JavaScript 原始碼。C/C++ 的世界有 ELF、PE、Mach-O 等二進制格式,但沒有一種能安全地在瀏覽器沙箱裡執行。asm.js 用「JS 的子集 + 型別標註」硬湊出近似二進制的效能,但解析與驗證依然慢——瀏覽器需要一種原生支援的二進制格式。
- 跨語言的渴望。 遊戲開發者想用 C++、科學計算想用 Python、系統工程師想用 Rust——而瀏覽器只說一種語言。誰遇到?每一個想把桌面級應用搬上網頁的人。
為何重要?因為瀏覽器已經成為世界上最大的應用平台,而它只有一門語言。2017 年,四大瀏覽器(Chrome、Firefox、Safari、Edge)同時支援 WebAssembly 1.0——這是瀏覽器史上第一次所有廠商一致通過的新標準。
偵查過程#
WebAssembly(WASM)的推理是一場 VM 技術的「歷史總匯」——它的每個設計決策都能在歷史上找到前身:
推理一:棧式虛擬機位元組碼。 WASM 是一種棧式虛擬機的指令集,血統直接承襲 2000-Csharp與DotNet.md 的 CIL、JVM 位元組碼與更早的 p-code。以兩數相加為例,WASM 的文字格式(WAT):
(module
(func (export "add") (param $a i32) (param $b i32) (result i32)
local.get $a ;; 推上堆疊
local.get $b ;; 推上堆疊
i32.add ;; 彈出兩值相加
)
)
與 CIL 的 ldc.i4/add 幾乎同構——VM 時代積累的位元組碼技術,三十年後被瀏覽器完整收割。
推理二:靜態型別 + 可驗證性。 WASM 指令帶有明確的型別(i32、i64、f32、f64),整個模組在載入時可以在線性時間內完成驗證——這是對 JS「先跑再說」的正面反擊。驗證的核心推導:
推理三:沙箱與記憶體模型。 WASM 的記憶體不是任意指標,而是一塊連續的線性記憶體(linear memory),只能透過索引存取:
這條規則讓 C++ 的指標被「閹割」成安全的索引——段錯誤被沙箱邊界攔截,逃不出瀏覽器。 這與 2010-Rust.md 的借用檢查器異曲同工:一個靠型別系統、一個靠沙箱,殊途同歸地消滅了不安全的記憶體存取。
推理四:二進制 + 文字雙格式。 WASM 有緊湊的二進制格式(.wasm,載入與解析都快)與人類可讀的文字格式(.wat)。二進制格式是給機器的(近似原生效能),文字格式是給人類的(可除錯、可手寫)。
推理五:從 asm.js 到 WASM 的演進。 Zakai 的 asm.js(2013)證明了「JS 子集 + AOT 編譯」可行;WASM(2016)則是四大瀏覽器聯手,把這條路升級為原生標準。Emscripten 工具鏈讓 C/C++ → JS/WASM 的遷移幾乎自動化。
結案報告#
WASM 的遺產仍在快速擴張:
- 瀏覽器裡的第二語言生態。 C/C++(Emscripten)、Rust(wasm-bindgen、wasm-pack)、Go、C#(Blazor)、Python(Pyodide)、Ruby、Kotlin 全部可以編譯到 WASM。瀏覽器從「JS 獨裁」變成「多語言共享執行環境」——這正是 CLR 在 2000 年的願景,二十年後在瀏覽器裡實現。
- 從瀏覽器到通用執行環境。 WASI(WebAssembly System Interface)讓 WASM 跑到瀏覽器之外:伺服器(Fastly 的 compute@edge)、邊緣運算、嵌入式系統、區塊鏈(以太坊的 eWASM 設計)。Docker 創始人 Solomon Hykes 的名言:「如果 WASM + WASI 早在 2008 年就存在,我們根本不需要發明 Docker。」WASM 正在成為雲端時代的新容器格式。
- 效能天花板被打破。 Figma、Photoshop for Web、Google Earth、各種遊戲引擎都在 WASM 上跑出了接近原生的體驗。
- 影響後續技術:見 2020-AI時代的程式語言.md——WASM 讓 Python 的 ML 生態可以在瀏覽器裡跑(Pyodide + PyTorch 的邊緣化嘗試),也讓「程式碼在哪裡執行」這個問題有了新的答案:哪裡都可以。
2016 年的 WASM 用一堆棧式指令與一塊線性記憶體,回答了 JavaScript 的效能天花板之謎——瀏覽器自此有了自己的虛擬機。
證據與工具#
以下先展示 WAT(WebAssembly 文字格式)的經典範例:
(module
;; 遞迴計算階乘:C 函數的 WASM 化
(func $fact (export "fact") (param $n i64) (result i64)
(if (result i64)
(i64.lt_s (local.get $n) (i64.const 2))
(then (i64.const 1))
(else
(i64.mul
(local.get $n)
(call $fact (i64.sub (local.get $n) (i64.const 1)))))))
;; 線性記憶體:指標被閹割成索引
(memory (export "mem") 1) ;; 1 頁 = 64KB
(data (i32.const 0) "hello from wasm")
)
再用 Python 模擬 WASM 的棧式 VM 執行引擎(驗證 + 執行):
# 模擬極簡 WASM VM:棧式機 + 型別驗證
TYPE = {"i32.add": ("i32", "i32", "i32"),
"i64.mul": ("i64", "i64", "i64"),
"f64.add": ("f64", "f64", "f64")}
def run_wasm(instructions, types={}):
stack, output = [], []
for ins in instructions:
if ins.startswith(("i32.const", "i64.const", "f64.const")):
kind, val = ins.split()
stack.append((kind, float(val) if "." in kind else int(val)))
elif ins in TYPE: # 型別驗證:線性時間
k, ta, tb = TYPE[ins]
b = stack.pop(); a = stack.pop()
if a[0] != ta or b[0] != tb:
raise TypeError(f"type mismatch: {a[0]} {b[0]} for {ins}")
r = {"add": lambda: a[1] + b[1], "mul": lambda: a[1] * b[1]}[k.split(".")[1]]()
stack.append((ta, r))
elif ins == "local.get":
stack.append(types[ins.split()[1]])
return stack
# fact(5) 用迴圈展開:5 * 4 * 3 * 2 * 1 的 i64 運算
program = ["i64.const", "5", "i64.const", "4", "i64.mul",
"i64.const", "3", "i64.mul",
"i64.const", "2", "i64.mul",
"i64.const", "1", "i64.mul"]
print(run_wasm(program)) # [('i64', 120)]
再模擬 WASM 的線性記憶體沙箱——越界存取被 trap 攔截:
class Trap(Exception): pass
class LinearMemory:
def __init__(self, pages=1):
self.size = pages * 65536 # 每頁 64KB
self.mem = bytearray(self.size)
def load(self, addr, n=1):
if addr + n > self.size: # 沙箱邊界:越界即 trap
raise Trap(f"out of bounds: load at {addr}")
return bytes(self.mem[addr:addr+n])
def store(self, addr, data):
if addr + len(data) > self.size:
raise Trap(f"out of bounds: store at {addr}")
self.mem[addr:addr+len(data)] = data
mem = LinearMemory(pages=1)
mem.store(0, b"hello wasm")
print(mem.load(0, 5)) # b'hello'
try:
mem.load(65000, 100) # 越界:C 的段錯誤在這裡被攔截
except Trap as e:
print("Trap:", e)
2016 年的 WebAssembly 用三十年積累的 VM 技術,在瀏覽器裡重建了一個安全的通用執行環境——VM 時代的最後一塊拼圖,就此歸位。