⭐⭐⭐⭐⭐ 5星

Rust WASM Parser 重写为 TypeScript 反而快3倍

来源: OpenUI 技术博客 | 日期: 2026-03-13

OpenUI 团队将 Rust WASM 解析器重写为 TypeScript,性能提升 2.2x-4.6x。这是一个令人深思的性能优化案例,核心发现是:WASM 边界开销才是真正的瓶颈

核心教训: 优化错误的东西而忽略了真正瓶颈,是常见的性能优化误区。

问题的本质:WASM 边界税

团队最初的想法很合理:Rust 很快,WASM 让代码在浏览器中接近原生速度。但实际测试发现,每次调用 WASM 解析器都要支付固定开销:

JS world        WASM world
────────────────────────────────────────────────────────
wasmParse(input)
│
├─ copy string: JS heap → WASM linear memory (allocation + memcpy)
│
│ Rust parses ✓ fast
│ serde_json::to_string() ← serialize result
│
├─ copy JSON string: WASM → JS heap (allocation + memcpy)
│
JSON.parse(jsonString) ← deserialize result
│
return ParseResult

Rust 解析本身从来不是慢的部分。 开销完全在边界:字符串复制进、序列化结果为 JSON、字符串复制出、然后 V8 反序列化为 JS 对象。

失败的尝试:跳过 JSON 往返

结果:反而慢了 30%!

团队尝试使用 serde-wasm-bindgen 让 WASM 直接返回 JS 对象,跳过 JSON 序列化。但事实证明这是错误的方向。

原因:JS 无法直接读取 WASM 线性内存中的 Rust 结构体。serde-wasm-bindgen 必须递归地将 Rust 数据物化为真实的 JS 数组和对象,这涉及每次 parse() 调用期间许多细粒度的跨运行时边界转换。

相比之下,JSON 方案中 serde_json::to_string() 在纯 Rust 中运行,零边界穿越,产生一个字符串,一次 memcpy 复制到 JS 堆,然后 V8 的原生 C++ JSON.parse 以单一优化流程处理。 更少、更大、更优化的操作胜过多而小的操作。

基准测试对比

JSON 往返 vs 直接 JsValue

FixtureJSON 往返serde-wasm-bindgen变化
simple-table20.5µs22.5µs-9%
contact-form61.4µs79.4µs-29%
dashboard57.9µs74.0µs-28%

TypeScript vs WASM (重写后)

FixtureTypeScriptWASM加速比
simple-table9.3µs20.5µs2.2x
contact-form13.4µs61.4µs4.6x
dashboard19.4µs57.9µs3.0x

另一个问题:O(N²) 流式处理

消除 WASM 解决了每次调用成本,但流式架构还有一个更深层的低效问题。

解析器在每个 LLM chunk 上调用。朴素的方法是累积 chunks 并每次从头重新解析整个字符串。这导致了 O(N²) 复杂度——输入越长,每次解析花费的时间呈二次方增长。

解决方案是实现增量解析算法,只解析新 chunk 并将其合并到现有解析树中。

💡 核心洞察

"Fewer, larger, and more optimized operations win over many small ones"

选择正确的抽象层比优化局部性能更重要。在浏览器环境中,原生 JS 运行时优化往往超过跨边界开销。

相关资源


🧬 探索任务发现 | 2026-03-21 08:30