Rust WASM Parser重写为TypeScript反而快3倍
摘要
OpenUI团队将Rust WASM解析器重写为TypeScript,性能提升2.2x-4.6x。核心发现:WASM边界开销才是真正的瓶颈,而非Rust代码本身的执行速度。
核心亮点
- WASM边界税 - 字符串复制进/出、JSON序列化/反序列化开销远超Rust解析本身
- serde-wasm-bindgen反而更慢30% - 试图跳过JSON往返反而因为跨边界细粒度转换更慢
- 纯TypeScript方案 - 消除边界后,contact-form从61.4µs降到13.4µs (4.6x)
- O(N²)流式问题 - 每个LLM chunk重新解析整个字符串,后来用增量算法解决
核心洞察: "Fewer, larger, and more optimized operations win over many small ones"
性能对比
| 测试用例 | WASM (µs) | TypeScript (µs) | 加速比 |
|---|---|---|---|
| simple-table | 20.5 | 9.3 | 2.2x |
| contact-form | 61.4 | 13.4 | 4.6x |
| dashboard | 57.9 | 19.4 | 3.0x |
问题分析
第一次尝试失败:使用serde-wasm-bindgen直接返回JS对象,反而比JSON方案慢30%。原因是JS无法直接读取Rust struct的字节,需要递归跨边界转换。
WASM边界开销
- JS → WASM:字符串复制 + 内存分配 + memcpy
- WASM → JS:序列化 + 字符串复制 + 内存分配 + memcpy
- JSON.parse:V8 C++优化,处理效率高
解决方案:消除WASM边界,完全在V8中运行。JSON序列化在纯Rust中完成,单次memcpy,V8的JSON.parse是高度优化的C++代码。
流式解析优化
第二个问题是流式场景下的O(N²)复杂度:每个LLM chunk都重新解析整个累积字符串。
解决方案:语句级增量缓存
- 以depth-0换行符结束的语句是不可变的
- 缓存已完成语句的AST
- 只重算尾部进行中的语句
- O(total_length)复杂度替代O(N²)
教训
团队最初优化了错误的东西(Rust解析速度),而忽略了真正的瓶颈(边界开销)。这个案例提醒我们:
- 先测量,再优化
- 跨边界开销可能比预期大得多
- V8的优化可能超出预期