Vize

パフォーマンス

⚠️ 開発中: Vize は現在も開発を進めており、まだ本番環境での利用に向けた準備は整っていません。ベンチマークは開発版で計測した数値であり、今後変わる可能性があります。

Vize は Rust のゼロコスト抽象化とネイティブのマルチスレッド処理を使い、標準の JavaScript 製 Vue コンパイラより高速に処理します。開発を快適に進めるうえで、処理速度は欠かせない要件です。

ベンチマーク環境

README と全言語のページは、同じコミット済み成果物から生成します。ファイル数は各行に記載しています。

ランナー 計測日時 計測回数 ウォームアップ
blacksmith-32vcpu-ubuntu-2404 (32 CPU) 2026-09-16T08:19:25.262Z 3 1

Actions · f9cc50ee2e2f · JSON

以下のローカル環境での Linter・Formatter・実装プロファイルは過去の計測で、このスナップショットの結果ではありません。記録された環境は MacBook Pro M2 Max (12 コア、96 GB RAM)、macOS 15.3.2、Node v24.14.0、Vite v8.0.0、Vue v3.6.0-beta.10 です。

ベンチマーク: SFC コンパイル

ファイル数: 3,000 (12,302,100 bytes).

@vue/compiler-sfc Vize 速度比
1T 3.57s 835.3ms 4.3x
workers / max 1.61s 65.0ms 24.7x
1T / max 3.57s 65.0ms 54.8x

詳しい計測方法と検証結果

ツールのバージョンとバイナリのチェックサム
ツール バージョン バイナリ SHA-256
Vize vize 0.424.11 dc5e840133beec86106ca27c496ca01fbff4299a8aef6c3a0f2fca3131f88fe3
tsgo Version 7.0.2 4f2de678286401759b3fb4475bafe35b8f32b4b3a07d92642bbf37eadc9b34a4
vue-tsc Version 6.0.3 811cc8124f36b6c770964d486da62af1b9db6949f62bc47205982d17e8e7dc8d
verter-tsc verter-tsc 0.0.1-beta.3 81bec5c957487bd2caf15312bf4f180afadd074f11c1d34cb4bb2c66bd7ac224
Golar Golar version 0.1.10 a100c0e559b6cf85fc3e67a65f0b8eedcee0ccf624022fc4882bea2a3d6316da
TypeScript (vue-tsc) n/a n/a
Vue 3.6.0-beta.10 n/a
ESLint v10.4.1 41dd060d45aca4a2144b94862e21e285bbb726b19e61db7a03c5b2e0b67973cc
Prettier 3.8.3 a7a78ad15344be4f14f24832a8c5fa5918bec4339b8d1d0903a5167c34ecb428
Node.js v24.14.0 n/a

バックエンド: ネイティブ TypeScript エンジンのパスは /home/runner/_work/vize/vize/node_modules/@typescript/typescript-linux-x64/lib/tsc です。型検査の計測では、意図的に追加した診断が検出されることを tools/benchmarks/scripts/check-gate.mjs(.github/workflows/check-bench.yml)で検証します。

なぜ Rust なのか?

ゼロコストの抽象化

Rust の所有権モデルにより、ガベージコレクションの一時停止が不要になります。テンプレートの AST ノードはコンパイルごとのアリーナ (vize_carton) に置かれ、テキストはテンプレートのソースから借用します。つまりノードは純粋なデータであり、それ自身が所有するヒープ割り当てを持ちません (crates/vize_relief/src/relief/elements.rs)。次の特徴があります。

  • GC による停止がない — V8 ベースのコンパイラでは、ガベージコレクションにより予測できない急な遅延が発生する可能性があります。Vize には GC オーバーヘッドがありません。
  • JIT ウォームアップなし — V8 の JIT コンパイラは頻繁に実行する処理を最適化するのに時間がかかります。Vize は最初の命令から全速力で実行されます。
  • 予測可能なパフォーマンス — Rust は事前にコンパイルするため、V8 による実行時の最適化判断に依存せず、実行ごとの性能が安定します。

ネイティブのマルチスレッド処理

Vize はデータ並列コンパイルに Rayon を使用します。各 SFC ファイルは独立してコンパイルできるため、並列処理に適しています。crates/vize/src/commands/build/runner.rs のバッチドライバーが入力をスレッドプールに振り分けます。

// crates/vize/src/commands/build/runner.rs — バッチドライバー
planned_inputs
    .par_iter()
    .map(|input| compile_file_with_profile(&input.source, compile_settings, &stats))
    .collect()

この処理ではアリーナを作成しません。vize_atelier_sfc 内でテンプレート、スクリプト、スタイルのコンパイルを開始する際に、ワーカーごとのプールから取得します。

// 例: crates/vize_atelier_sfc/src/compile.rs
let allocator = vize_carton::pool::acquire();

ワークスティーリングでは、処理を終えたスレッドが、ほかのスレッドのキューに残っている作業を引き受けます。大きなファイルと小さなファイルが混在していても、負荷を均等に分散できます。

効率的なメモリレイアウト

Rust の構造体レイアウトと列挙型判別子はコンパクトです。vize_relief の AST 表現はキャッシュに適しており、メモリ帯域幅の制約による影響を抑えます。

  • 1 バイトの判別子 — NodeType は 27 個のバリアントを持つ #[repr(u8)] です (crates/vize_relief/src/relief/core.rs)。ノードの種別はヒープに割り当てられた文字列ではなく 1 バイトで済みます。
  • 固定されたノードサイズ — すべてのテンプレートノードにサイズの const アサーションが付いており、フィールドの追加で規定のサイズを超えると、ビルド時に検出できます。ElementNode は 104 バイト、SimpleExpressionNode は 88、AttributeNode は 56、TextNode は 24、SourceLocation は 8 です (crates/vize_relief/src/relief/{elements,expressions,control_flow,nodes}.rs)。
  • オブジェクトヘッダーなし — JavaScript オブジェクト (プロトタイプチェーン、プロパティマップ、隠しクラスへのポインタを持つ) とは異なり、Rust の構造体はオーバーヘッドのない純粋なデータです。

実行時のオーバーヘッドなし

V8 で実行される JavaScript 製コンパイラとは異なり、Vize はネイティブコードに直接コンパイルします。JIT ウォームアップ、ガベージコレクタ、イベントループの競合はありません。CLI はプラットフォームごとに自己完結型のネイティブ実行可能ファイルとして配布されます。musl Linux ターゲットでは完全に静的リンクされており、CI がそれを検証しています (tools/commands/ci/github/verify-musl-cli-binary.rs)。glibc、macOS、Windows の各ターゲットでは、システムの C ライブラリに動的リンクされます。Vite プラグインは、同じコンパイラを別プロセスではなくネイティブの Node アドオン (@vizejs/native) として読み込みます。

性能を支える設計

アリーナによるメモリ割り当て

vize_carton::Allocator は AST ノード用のバンプアロケータであり、oxc_allocator をラップすることで、テンプレートノードと保持された JavaScript 式が 1 つのアリーナと 1 つのライフタイムを共有します (davinci/vize_l0/src/allocator.rs)。次の特徴があります。

  • 割り当ては O(1) — ポインタを前方に移動するだけです。フリーリストの走査や断片化の管理はありません。
  • 回収も O(1) で、メモリを再利用できる — コンパイルの終わりにアリーナは破棄されるのではなく reset() されます。バンプポインタはチャンクの先頭に戻り、アリーナはワーカーごとのフリーリストに返されます (davinci/vize_l0/src/pool.rs、ワーカーあたり最大 4 個の待機中のアリーナ)。次のファイルは OS に追加のメモリを要求せず、同じメモリを再利用します。
  • メモリの局所性が優れている — ノードをメモリ上に連続して配置することで、ツリー走査時に L1/L2 キャッシュを効率よく利用できます。

アリーナに置かれた値は、そのコンパイル処理が終わった後も存続することはできません。この制約は Rust コンパイラが検証し (reset は &mut self を取り、プールのガードは自身のアリーナを所有します)、デバッグビルドでは世代スタンプでも検証します。アリーナの再利用後に古い値を読み出すとパニックします (davinci/vize_l0/src/allocator/generation.rs)。

AST には Drop を実装した型が 1 つもありません。アリーナのコンテナ型はドロップが必要なペイロードを拒否するため、これは慣習ではなくコンパイルエラーになります。

1 回の走査で処理するトークナイザー

vize_armature のトークナイザーは &[u8] 上のバイト指向ステートマシンです (crates/vize_armature/src/tokenizer.rs)。トークンを実体化することは一切ありません。コンパイラのどこにも Token 型もトークンのベクタも存在しません。代わりに tokenize() が入力の末尾まで 1 回走査し、パーサーが実装する Callbacks の実装へイベントを送ります。各イベントは生成されたその場で同期的に処理され、トークン化と構文解析を分ける設計で必要になる中間配列を作らずに済みます。

トークナイザーからパーサーへイベントを送るプッシュ型です。パーサーが必要に応じてトークンを取り出す方式ではないため、パーサー側から走査を途中で止めることはできません。

文字列インターン

1 回のコンパイル内で繰り返し現れる名前 — 正規化されたディレクティブ名、アセット名、キャメルケース化された引数名 — は vize_carton::interner によってアリーナ上のアトムにインターンされます。コンパイル時の phf セットには 181 個の既知の名前 (HTML/SVG/MathML のタグ、Vue の組み込みコンポーネント、ディレクティブ名、変換処理で特別に扱う属性) が入っており、これらはアリーナに一切触れずに 'static リテラルへ解決されます。次の特徴があります。

  • 繰り返される計算済みの名前は、1 つのアリーナ割り当てを共有します
  • 既知の名前の探索はコンパイル時の完全ハッシュであり、割り当てを伴いません

インターンは一般的な経路ではなくフォールバックです。ほとんどの名前はそもそもコピーされません。タグ名、属性名、そして式の内容の多くは、テンプレートのソースから直接借用した &'a str スライスであり、通常の経路では何も割り当てません (フィールドごとの方針は davinci/vize_l0/src/interner.rs に記載されています)。

アトムは通常の &'a str であるため、名前の比較はポインタの同一性ではなく内容の比較です。インターンがもたらすのは割り当ての削減とキャッシュ局所性であり、== の高速化ではありません。

インクリメンタルコンパイル

Vite プラグイン (@vizejs/vite-plugin) は、ファイル単位でメモリとディスクの 2 層にキャッシュします。プリコンパイル時には、別にファイルの変更も検出します。

  • 開発と HMR 用のメモリ内キャッシュ — 解決済みのファイルパスをキーとします (npm/builder/vite/src/plugin/compiled-module-cache.ts)。ホットアップデート時に該当するエントリを明示的に破棄するため、変更されたファイルだけが再コンパイルされ、その隣接ファイルは再コンパイルされません。
  • プリコンパイルの変更検出 — mtime とサイズをキーとし、比較は Rust 側で行われます (crates/vize_atelier_sfc/src/vite_plugin/precompile.rs)。バッチが再コンパイルするファイルを決めるのはこのゲートです。
  • プロセスをまたいで使うディスクキャッシュ — node_modules/.vize/vite-precompile に置かれ、ソースの SHA-256 ハッシュに加えて、コンパイラのバイナリの同一性と解決済みオプションを含むマニフェストキーを使います (npm/builder/vite/src/plugin/precompile-cache-key.ts)。ここで内容ハッシュを使うのは、mtime がマシンやチェックアウトをまたいで信頼できないからです。

実測: アリーナと式の処理

上記のコンパイラ内部の処理は、クレートごとのマイクロベンチマーク (cargo bench --bench davinci) によって、規模や構造の異なる 6 つの固定フィクスチャ tools/benchmarks/crates/davinci_harness/fixtures/{small,medium,large,stress-deep,stress-wide,stress-interp}.vue 上で測定されます。

これらの数値の読み方。 割り当て回数は決定論的でマシンに依存しないため、同じ条件で正確に比較でき、回帰の検出に使います。実行時間は共有の開発マシン上で --quick サンプリングにより測定されたもので、方向性を示すだけです。基準とするランナー (Blacksmith) での実行時間の記録はまだありません。docs/davinci/plan/budgets.toml の wall_p50_ns と allocs は別々に読みます。0 は、実行時間では基準値が未記録であることを、割り当て回数では実測で割り当てがなかったことを示します。割り当て回数の基準値は既に記録されており、実行時間の基準値の有無にかかわらず、回帰の検出に使います。実行ごとの結果ファイルは tools/benchmarks/results/davinci/ に出力されますが、これはローカルの成果物であり、コミットされたベースラインではありません。

文字列とアリーナの処理を変更する前後で、コンパイル 1 回あたりのメモリ割り当て回数を比較しました。同じフィクスチャによる正確な回数です。

フィクスチャ パース DOM コンパイル SSR コンパイル Vapor コンパイル
small 21 → 9 52 → 39 73 → 60 90 → 73
medium 171 → 107 329 → 264 1,099 → 1,030 588 → 515
large 350 → 272 656 → 573 1,106 → 983 1,136 → 1,003
stress-deep 397 → 155 669 → 426 612 → 369 764 → 514
stress-wide 213 → 204 255 → 245 416 → 405 280 → 261
stress-interp 616 → 105 1,048 → 536 3,149 → 2,637 1,495 → 974

ノードのサイズもそれに伴って縮小し、新しいサイズは const アサーションで固定されています。RootNode 296 → 224 バイト、DirectiveNode 208 → 176、ElementNode 128 → 104、SimpleExpressionNode 120 → 88、AttributeNode 80 → 56、TextNode 32 → 24。

ピーク常駐メモリ。 最も大きな効果があったのは、ファイルをまたいだアリーナの再利用です。ここで示すのは、速度ではなくメモリ使用量の改善です。コミット済みコーパスの 36,541 個の SFC すべてをコンパイルした場合 (vize build "tests/_fixtures/_git/**/*.vue" --format stats、ci-opt バイナリ、/usr/bin/time -l の最大常駐セットサイズ、前後で同一マシン):

ワーカー数 変更前 変更後 変化 各実行回数
12 766.5 MB 171.1 MB −77.7% 5
1 717.0 MB 88.2 MB −87.7% 3

ワーカーが 1 個の場合はスケジューリングの影響を受けません。この条件でのメモリ使用量の減少から、以前はワーカー単位のアリーナではなく、ファイルを処理するたびにメモリが蓄積していたことが分かります。実行時間はノイズの範囲内で変化なく、出力された 36,541 ファイルはすべてバイト単位で同一でした (SHA-256 マニフェストを比較)。

式の再パース。 テンプレートの式はテンプレート解析時に一度だけ解析され、ノードに保持されるようになりました。後続の処理は、テキストを再解析せず、保持された AST を読みます。SSR の処理では stress-interp フィクスチャのコンパイル 1 回あたりの冗長な式の再解析が 500 回からゼロになり、解析と後続処理を合わせた実行時間は、AST を保持する変更を加える前と比べて −13.6% となりました (346.8µs → 299.8µs)。解析のコストは増えましたが、後続処理のコストはそれ以上に減っています。DOM と Vapor の処理にはそのフィクスチャで削除できる再解析がなかったため、増えた解析コストを依然として負担しています。この追加コストの解消は今後の課題であり、達成済みの改善には含めていません。

ベンチマーク: Linter — Patina vs eslint-plugin-vue

15,000 個の Vue SFC ファイルのリンティング (ローカルワークステーション):

eslint-plugin-vue (ST) Vize Patina (ST) 速度比 eslint-plugin-vue (MT) Vize Patina (MT) 速度比 eslint ST 対 Vize MT
時間 45.08秒 4.02秒 11.2x 16.38秒 784ミリ秒 20.9x 57.5x

再現するには、vp run --workspace-root bench:lint を実行します。

型情報を使う lint のプロファイル

型情報を使う lint では、SFC の解析、Croquis による解析、仮想 TypeScript の生成、テンプレート内の問い合わせ対象の収集、Corsa への問い合わせといった処理に時間がかかります。テンプレートの型情報を使うルールが複数有効な場合、Patina は AST を 1 回走査してテンプレート式と Promise の問い合わせ対象をまとめて収集し、その後 Corsa に問い合わせます。安全でないテンプレート式と、処理されていない Promise の検査では、OXC による式の解析結果も共有します。両方のルールを有効にしても、同じテンプレート式を重複して解析しません。

vize lint --profile --preset opinionated src を実行すると、手元のプロジェクトで各処理の所要時間を確認できます。プロファイルの冒頭には、計測対象が全体の経過時間をどれだけカバーしているか、各ワーカーの累積時間、低速判定のしきい値を超えた回数、内部処理の計測範囲を確認するための項目があります。続くファイル別の結果には、各処理の時間の割合とスループットを表示します。操作別の結果には、時間のかかる内部処理や、最大時間と平均時間の偏りを表示します。

ベンチマーク: フォーマッタ — Glyph と Prettier

15,000 個の Vue SFC ファイルのフォーマット (ローカルワークステーション):

Prettier (CLI) Vize Glyph (ST) 速度比 Vize Glyph (MT) Prettier CLI と Vize MT
時間 101.20秒 2.97秒 34.1x 835ミリ秒 121.2x

再現するには、vp run --workspace-root bench:fmt を実行します。

ベンチマーク: 型検査

README と全言語のページは、同じコミット済み成果物から生成します。ファイル数は各行に記載しています。

ランナー 計測日時 計測回数 ウォームアップ
blacksmith-32vcpu-ubuntu-2404 (32 CPU) 2026-09-16T08:19:25.262Z 3 1

Actions · f9cc50ee2e2f · JSON

対象 ファイル数 比較ツール 比較ツールの中央値 Vize 1T Vize max 速度比
型検査 500 vue-tsc 6.75s 2.03s 1.37s 4.9x
巨大 SFC 型検査 1 vue-tsc 1.34s 186.4ms 190.1ms 7.0x

型検査の速度比は、Vue プロジェクトが実際に使っている型チェッカー vue-tsc との比較です。vue-tsc は JavaScript 版 TypeScript コンパイラを、Vize はネイティブ tsgo を動かすため、この倍率にはツールチェーン全体の差が含まれ、Vue レイヤ単体の差ではありません。Vue レイヤだけを見たい場合は、詳細スナップショットのエンジンクラス別の表で同一エンジン同士の順位を比較してください。各計測で診断処理の検証を通過していますが、ツール間で診断の対応範囲は異なります。精度の同等性や、過去の Vize からの性能改善を示す倍率ではありません。検証を通過しなかった比較ツールには、時間や順位を掲載しません。

詳しい計測方法と検証結果

以下のプロファイルは過去のローカル計測であり、今回の比較には含まれません。

型チェッカーのプロファイル

500 個の SFC を使うフィクスチャでは、処理時間の大半を Corsa CLI コマンドが占めます。一方、import の書き換えを省略できるファイルでは、従来必要だった OXC の解析が不要になりました。これは、Vue のモジュール指定子を含まないファイルに適用される高速化です。

メトリック 前 現在
canon.import.rewrite.vue 26.77ミリ秒 2.45ミリ秒
生成された最大の仮想 TS 15,401B 14,414B
プロファイルの合計経過時間 1.88秒 668ミリ秒
Corsa 診断フェーズ 1.67秒 482ミリ秒
Corsa CLI 解析 該当なし 10.41ミリ秒

Rust 側の virtual project フェーズでは、ファイルごとに SFC の解析、Croquis による解析、仮想 TS の生成、import の書き換えを行います。VirtualProject::register_paths がこれらの処理を rayon のスレッドプールに振り分けます。ワークスペースのオプションを解決した後は、各 .vue ファイルを独立して処理できるため、バッチ内で並列に実行できます。1,000 個の SFC を使うフィクスチャでは、Corsa を呼び出す前のこの処理が約 71 ms から約 25 ms に短縮されました。

診断が多いアプリケーションでの E2E 計測

tools/benchmarks/scripts/check.ts は、フィクスチャが存在する場合、tests/_fixtures/_git/npmx.dev アプリも測定します。実際のアプリケーションを使い、診断を元のソースへ対応付ける処理も計測します。

フィクスチャ ソース SFC ファイル 仮想ファイル 診断 Vize Canon
npmx.dev アプリ 134 226 1,053 1.94秒

このフィクスチャでは、CLI の診断結果を解析する時間は約 7 ミリ秒で、処理時間の大半は Corsa CLI コマンドが占めます。フレームワークの自動インポート用スタブを 1 つのアンビエント宣言ファイルにまとめたことで、生成される仮想 TS ファイルの最大サイズも約 275 KB から 144 KB に減りました。

ベンチマーク: Vite プラグイン

README と全言語のページは、同じコミット済み成果物から生成します。ファイル数は各行に記載しています。

ランナー 計測日時 計測回数 ウォームアップ
blacksmith-32vcpu-ubuntu-2404 (32 CPU) 2026-09-16T08:19:25.262Z 3 1

Actions · f9cc50ee2e2f · JSON

ファイル数: 1,000.

@vitejs/plugin-vue @vizejs/vite-plugin 速度比
Vite ビルド 1.66s 889.2ms 1.9x

詳しい計測方法と検証結果

それまでここに掲載していた 957ms / 479ms / 2.0x は、#3392 以前の tools/benchmarks/scripts/vite.ts によるものでした。このハーネスは、ウォームアップが残した永続プリコンパイルキャッシュ付きの Vize と、ゼロからコンパイルする @vitejs/plugin-vue を比較していました。現在このハーネスは実行マシン上でコールドとウォームを別々に報告するため、その出力はローカルな診断値であり、公開できる速度比ではありません。vp run --workspace-root bench:vite は変更前後の自己比較に使ってください。