We rewrote our Rust WASM parser in TypeScript and it got faster

While Rust and WebAssembly are often synonymous with speed, a development team found that porting their parser to TypeScript actually increased performance by up to 4.6x by eliminating the costly overhead of the WASM-JS boundary.
We built the openui-lang parser in Rust and compiled it to WASM. The logic was sound: Rust is fast, WASM gives you near-native speed in the browser, and our parser is a reasonably complex multi-stage pipeline. Why wouldn't you want that in Rust?
Turns out we were optimising the wrong thing.
The openui-lang parser converts a custom DSL emitted by an LLM into a React component tree. It runs on every streaming chunk — so latency matters a lot. The pipeline has six stages: Autocloser, Lexer, Splitter, Parser, Resolver, and Mapper.
Every call to the WASM parser pays a mandatory overhead regardless of how fast the Rust code itself runs. The overhead was entirely in the boundary: copy string in, serialize result to JSON string, copy JSON string out, then V8 deserializes it back into a JS object.
We tried integrating serde-wasm-bindgen to return a JS object directly, skipping JSON. It was 30% slower. JS cannot read a Rust struct's bytes from WASM linear memory as a native JS object. To construct a JS object from Rust data, serde-wasm-bindgen must recursively materialise Rust data into real JS arrays and objects, involving many fine-grained conversions across the runtime boundary.
We ported the full parser pipeline to TypeScript. Running entirely in the V8 heap eliminated the boundary cost.
| Fixture | TypeScript | WASM | Speedup | |---|---|---|---| | simple-table | 9.3 | 20.5 | 2.2x | | contact-form | 13.4 | 61.4 | 4.6x | | dashboard | 19.4 | 57.9 | 3.0x |
Furthermore, we optimized the streaming architecture. The naïve approach re-parses the entire string on every chunk (O(N²)). We added a streaming parser that caches completed statement ASTs, moving to O(N).
| Fixture | Naïve TS | Incremental TS | Speedup | |---|---|---|---| | contact-form | 316 | 122 | 2.6x | | dashboard | 840 | 255 | 3.3x |
Lessons Learned:
- Profile where time is actually spent: For us, the cost was data transfer, not computation.
- WASM is best for compute-bound tasks with minimal interop (image processing, crypto).
- WASM is poor for parsing structured text into JS objects because you pay the serialization cost either way, and V8's JIT is already very fast for JS logic.
- Algorithmic improvements (O(N²) to O(N)) dominate language-level optimizations.
Source: Hacker News









