世界を動かす技術を、日本語で。

Rust製のWASMパーサーをTypeScriptで書き直したら、速度が向上しました

2026年3月21日原文(openui.com)

概要

  • Rust製WASMパーサは、JSとのデータ受け渡しコストがボトルネック
  • serde-wasm-bindgenによる直接オブジェクト返却はJSON経由より遅い
  • TypeScript移植+インクリメンタルパースで劇的な高速化を実現
  • WASMは計算量が大きく、境界をまたぐ回数が少ない用途で最適
  • アルゴリズム改善が言語最適化より実効効果大

Rust製WASMパーサの構造と課題

  • openui-lang parser はRustで実装、WASMへコンパイル
  • LLMが出力する独自DSLをReactコンポーネントツリーへ変換
  • 6段階パイプライン構成
    • autocloser:途中テキストの構文補完
    • lexer:トークン化
    • splitter:id=式単位で分割
    • parser:AST生成
    • resolver:変数参照解決・循環検出
    • mapper:ASTをReact用OutputNode形式へ変換
  • 各パース呼び出しで必ず発生する JS↔WASM間のデータコピー が遅延要因

JSON経由とserde-wasm-bindgenの比較

  • 従来: serde_json::to_string() でRust側でJSON化→JSでパース
    • 大きな文字列1回のコピー+V8の最適化済みJSON.parse
  • serde-wasm-bindgen:Rust構造体を JsValue へ直変換し返却
    • しかしJSとRustのメモリレイアウトが異なり、フィールド毎に逐次変換が必要
    • 小さなデータを大量に変換するため 30%遅くなる ケースが多発

ベンチマーク比較(1000回実行、1回あたりの平均μs)

| Fixture | JSON往復 | serde-wasm-bindgen | 差分 | |----------------|----------|--------------------|------------| | simple-table | 20.5 | 22.5 | 9%遅い | | contact-form | 61.4 | 79.4 | 29%遅い | | dashboard | 57.9 | 74.0 | 28%遅い |

TypeScriptへの全面移植とさらなる最適化

  • TypeScript移植 :同じ6段階構成、WASM排除でV8ヒープ内で完結
  • ワンショットパース (1回のparse呼び出し)

| Fixture | TypeScript | WASM | 高速化倍率 | |----------------|------------|------|------------| | simple-table | 9.3 | 20.5 | 2.2x | | contact-form | 13.4 | 61.4 | 4.6x | | dashboard | 19.4 | 57.9 | 3.0x |

ストリーミング時の非効率性とインクリメンタルパース

  • LLMのストリーム出力ごとに 全体再パース (O(N²))が発生
    • 例:1000文字を20文字ずつ50回→累積25,000文字分パース
  • 解決策:文単位インクリメンタルキャッシュ
    • 完了した文ASTはキャッシュ、未完了分のみ再パース(O(N))
    • 完了文は再パース不要、進行中のみ都度パース

ストリーム全体のコスト比較(全チャンク合計μs)

| Fixture | Naïve TS | Incremental TS | 高速化倍率 | |----------------|----------|---------------|------------| | simple-table | 69 | 69 | 変化なし | | contact-form | 316 | 122 | 2.6x | | dashboard | 840 | 255 | 3.3x |

WASM活用の適正領域と教訓

  • WASMが向くケース
    • 計算集約型でインターフェース回数が少ない処理(画像処理、暗号、物理シミュレーション等)
    • 既存C/C++ライブラリのブラウザ移植(例:SQLite, OpenCV)
  • WASMが不向きなケース
    • 構造化テキストをJSオブジェクト化する処理(パース自体は高速、データ転送が律速)
    • 小さな入力に対し頻繁に呼ばれる関数(境界コストが打ち消す)
  • 重要な知見
    • serde-wasm-bindgenの「直接オブジェクト返却」はコスト削減にならない
    • アルゴリズム改善(O(N²)→O(N))が言語最適化より効果大
    • WASMとJSはヒープ共有不可、常に変換コストが発生

まとめ

  • WASM活用時は「どこで何に時間がかかるか」事前プロファイルが必須
  • 境界コストが支配的な用途ではTypeScriptの方が有利
  • アルゴリズム設計の見直しが最大の高速化要因

Hackerたちの意見

ここでの本当の勝利は、RustよりもTSのことじゃなくて、O(N²)からO(N)へのストリーミング改善なんだよね。これは言語の選択とは関係なく、単独で3.3倍の改善になる。WASMの境界を排除するのは2~4倍だけど、実際にユーザーが感じるストリーミング中のレイテンシーに影響を与えるのはアルゴリズムの修正なんだ。タイトルはもっと面白いエンジニアリングを過小評価してると思う。

もっと誤解を招くクリックベイトだね。

そうだね、でもn²はちょっと誇張してるかも。一つ気づいたのは、各呼び出しの時間を計ってから中央値を使ってること。ため息。ブラウザでね。:/ JSエンジンにタイミング攻撃防御が組み込まれてるのに。

タイトルはもっと面白いエンジニアリングを過小評価してると思う。クリックベイトを突き抜けてくれてありがとう。投稿は面白いけど、無駄にクリックベイトに引っかかって記事を読むのにはもう疲れたよ。

uvも同じだけど、誰もそのメッセージを受け取らないんだよね。みんな「Rust最高!」って思って、uvの利点が言語じゃなくてアルゴリズムだってことを無視してる。

O(N²)からO(N)にしたら3.3倍速くなったけど、その前に境界を排除して(wasmをJSに置き換えたら)2.2倍、4.6倍、3.0倍のスピードアップがあったよ(前のテーブルを見てみて)。どちらも「本当の勝利」ではないみたい。言語とアルゴリズムの両方が大きな違いを生んでるのが、最後のテーブルの最初の列でわかるよ。wasmに移行することで大きなスピードアップがあって、その上でアルゴリズムを改善したことでさらに大きなスピードアップが得られたんだ。

そうだね、アルゴリズムの修正がここでは大部分を担ってる。でも、そのパーサーを何百回も小さなストリーミングチャンクに対して呼び出すと、WASMの境界コストがすぐに積み上がっちゃうよ。同じことがC++をWASMにコンパイルした場合にも起こるだろうね。

それは間違ってないけど、その勝利はあんまり注目されないだろうね。クリックベイトとしては弱すぎる。

HNではAI生成のコメントはやめてください。

「このコードを言語Lから言語Mに書き換えたら、結果が良くなった!」って、当然だよね。絡まったり歪んだりしてたものを整理するチャンスだったし、知られている悪い決定を避けて、新しく発明されたより良いアプローチを適用できたんだから。だから、L = Mでも成り立つんだよ。スピードアップは言語にあるんじゃなくて、書き直しと再考にあるんだ。

それから、元のコードを見たことがない第三者が、TypeScriptのソリューションをRustに書き換えれば、さらに効果が出るね。

その通り。コードを同じ言語で書き直しても改善が見られるよね。

Hacker Newsで議論の続きを見る