Rust WASM Parser 重写为 TypeScript 反而快3倍
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 往返
团队尝试使用 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
| Fixture | JSON 往返 | serde-wasm-bindgen | 变化 |
|---|---|---|---|
| simple-table | 20.5µs | 22.5µs | -9% |
| contact-form | 61.4µs | 79.4µs | -29% |
| dashboard | 57.9µs | 74.0µs | -28% |
TypeScript vs WASM (重写后)
| Fixture | TypeScript | WASM | 加速比 |
|---|---|---|---|
| simple-table | 9.3µs | 20.5µs | 2.2x |
| contact-form | 13.4µs | 61.4µs | 4.6x |
| dashboard | 19.4µs | 57.9µs | 3.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