Rust製WASMパーサーをTypeScriptに書き直したら3倍高速になった
(openui.com)- Rustで書かれたWASMパーサーは構造的には高速だが、JS-WASM境界でのデータコピーとシリアライズのオーバーヘッドが性能ボトルネックとして判明
serde-wasm-bindgenによる直接オブジェクト返却はJSONシリアライズより9〜29%遅く、これはランタイム間の細かな変換コストによるもの- TypeScriptにパイプライン全体を移植すると、同一アーキテクチャで2.2〜4.6倍高速な単一呼び出し性能を達成
- ストリーミング処理では文単位キャッシュによる O(N²)→O(N) 改善で、2.6〜3.3倍高速な全体処理速度を確保
- 結果として、WASMは計算集約的・低頻度呼び出しに適しており、JSオブジェクトのパースや頻繁に呼ばれる関数には不向きであることが確認された
Rust製WASMパーサーの構造と限界
openui-langパーサーは、LLMが生成したDSLをReactコンポーネントツリーへ変換する6段階パイプラインで構成- 段階:
autocloser → lexer → splitter → parser → resolver → mapper → ParseResult - 各段階はトークン化、構文解析、変数解決、AST変換などを実行
- 段階:
- Rustコード自体は高速だが、JS↔WASM間の文字列コピー・JSONシリアライズ・デシリアライズが呼び出しごとに発生
- 入力文字列のコピー(JS→WASM)、Rust内部でのパース、結果JSONのシリアライズ、JSONのコピー(WASM→JS)、JSでのデシリアライズ
- この境界オーバーヘッドが全体性能を支配しており、Rustの計算速度自体はボトルネックではなかった
serde-wasm-bindgen の試みと失敗
- JSONシリアライズを避けるため、Rust構造体を直接JSオブジェクトとして返す
serde-wasm-bindgenを適用 - しかし30%遅くなることが観測された
- Rust構造体のメモリをJSが直接読めるわけではなく、ランタイム間でメモリレイアウトが異なるため、フィールド単位の変換が必要
- 一方でJSONシリアライズは、Rust内部で一度文字列を生成したあと、JS側で最適化された
JSON.parseにより処理される
- ベンチマーク結果
Fixture JSON round-trip 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への移行と性能向上
- 同じ6段階構造をTypeScriptへ完全移植し、WASM境界を取り除いてV8ヒープ内で直接実行
- 単一呼び出し基準のベンチマーク結果
Fixture TypeScript WASM 性能向上 simple-table 9.3µs 20.5µs 2.2倍 contact-form 13.4µs 61.4µs 4.6倍 dashboard 19.4µs 57.9µs 3.0倍 - WASMを外しただけで呼び出しあたりのコストは大幅に減少したが、ストリーミング構造の非効率は依然として残っていた
ストリーミングパースの O(N²) 問題と改善
- LLM出力が複数チャンクで渡される場合、毎回累積済みの全文字列を再パースするO(N²) の非効率が発生
- 例: 1000文字の文書を20文字ずつ50回パース → 合計25,000文字を処理
- 解決策として**文単位の増分キャッシュ(incremental caching)**を導入
- 完成した文はキャッシュし、進行中の文だけを再パース
- キャッシュ済みASTと新しいASTをマージして結果を返す
- ストリーム全体基準のベンチマーク
Fixture ナイーブTS 増分TS 性能向上 simple-table 69µs 77µs なし contact-form 316µs 122µs 2.6倍 dashboard 840µs 255µs 3.3倍 - 文が多いほどキャッシュ効果は大きくなり、全体スループットは線形に改善する
WASM利用に関する教訓
- 適しているケース
- 計算集約的で相互作用の少ない処理: 画像・動画処理、暗号化、物理シミュレーション、オーディオコーデックなど
- 既存ネイティブライブラリの移植: SQLite、OpenCV、libpng など
- 適していないケース
- JSオブジェクトへ構造化するテキストパース: シリアライズコストが支配的
- 短い入力を高頻度で呼び出す関数: 境界コストが計算コストを上回る
- 重要な教訓
- ボトルネックの位置をプロファイリングしてから言語を選ぶべき
serde-wasm-bindgenの直接オブジェクト受け渡しはより高コスト- アルゴリズム複雑性の改善は言語移行より効果的
- WASMとJSはヒープを共有せず、変換コストは常に存在する
最終結果: TypeScript移行と増分キャッシュにより、呼び出しあたり2.2〜4.6倍、ストリーム全体で2.6〜3.3倍の性能向上を達成
2件のコメント
高度なRust性能改善記事を遠回しに皮肉る趣旨だったのではないか..
Hacker Newsのコメント
本当の核心は、RustよりTypeScriptということではなく、O(N²)からO(N) に削減したストリーミングアルゴリズムの修正にある
文(statement)単位のキャッシュによるこの変更だけでも3.3倍の向上があった
言語選択とは別に、ユーザー視点で感じる レイテンシ 改善の主因はこの部分だ
タイトルはこうした興味深いエンジニアリング上のポイントを過小評価しているように感じる
記事自体は興味深いが、最近は過度な クリック誘導型タイトル にうんざりしている
各呼び出し時間を測定して 中央値(median) を使う方式だが、ブラウザ環境ではタイミング攻撃対策ロジックを持つJSエンジンなので正確性に疑問がある
「言語LからMへコードを書き直したら速くなった」という話は当然の結果だ
もつれた誤った判断を正し、新たに生まれた より良いアプローチ を適用する機会だったからだ
実際にはL=Mでも同じで、速度向上は言語ではなく リライトと再設計 の過程から生まれる
RustとJSの境界でオブジェクトシリアライズ性能を改善しようとして、さらに深く調べたことがある
serdeのアプローチは性能面であまり良くないように見え、それを改善する試みを 私のブログ記事 にまとめた
Open UIがWASM関連の作業をしていない理由が気になっていた
ところが今回の新しい会社が Open UI という名前を使っていて混乱した
もともとの Open UI W3C Community Group は、5年以上にわたってHTMLのpopover、カスタマイズ可能なselect、invoker command、accordionのような標準を作っているグループだ
彼らは本当に素晴らしい仕事をしている
「JSONの往復をスキップしよう」という試みでserde-wasm-bindgenを統合したらしいが、結局 バイナリ形式のJSONの再発明 のように見える
最近のV8のJSONはすでに 非常に最適化 されており、simdjson のような実装は毎秒ギガバイト級で処理できる
JSONがボトルネックである可能性は低いと思う
そのブログの デザイン が本当に気に入った
スクロール位置に応じて見出しをハイライトする「scrollspy」サイドバーが特に良かった
Claudeによると、fumadocs.dev で作られているようだ
Rust WASMパーサーの目的がよく分からなかった
記事ではその部分が明確でなく、もう少し説明が必要だ
これはプロンプトインジェクションによる情報漏えいを防ぐためのものと見られる
パーサーはLLMからストリーミングされるチャンクをコンパイルしてリアルタイムUIを構成する
以前はチャンクごとにパーサーを最初から再起動していたが、これを 段階的処理方式 に変えたことで(Rust→TypeScriptへの移植中に)性能が大きく向上した
TypeScriptが最近 Golangベース で動いているのではないかと疑問に思った
冗談だが、またRustにリライトすればさらに 3倍の性能向上 があるかもしれない /s