← 記事一覧へ

Tauriのケーパビリティはコールド起動を速くする — と思っていた。違った。

仮説があった:Tauri v2のケーパビリティを最小限に絞れば、コールド起動が速くなるはずだ。注入される__TAURI__ブリッジが小さくなり、パースが減り、初期化のコストが下がり、最初の描画が速くなる。

筋は通っているように見えた。ベンチマークを取った。仮説は死んだ。

この記事は、その代わりに見つけたものについての話だ。


1. 仮説

Tauri v2では、ケーパビリティ宣言がsrc-tauri/capabilities/配下のウィンドウごとのJSONファイルに分割された。「メインウィンドウはcore:defaultplugin:fs:defaultを呼べる。オーバーレイウィンドウはcore:defaultだけ」のように、コマンド単位での最小権限が表現できる。

Vaultzは既にその構成を持っていた:メインウィンドウ用のdefault.jsonと、パスワードオーバーレイ用に意図的に権限を絞ったoverlay.json。同じアプリで2種類のケーパビリティ。「ケーパビリティを狭めるとコールド起動が速くなるのか」を問うのに、これ以上ない実験環境だ。

直観はこうだ:ケーパビリティはランタイムのTauriコマンド認可チェックに使われる。狭いケーパビリティ → 小さな認可テーブル → 初期化するコードパスが少ない → 注入されるJSが小さい → 最初の描画が速くなる。

ベンチマークを書いた。直観は外れていた。


2. ベンチマーク

同じアプリ、同じマシン、2種類のビルド:

  • バリアントA — 狭いケーパビリティ(本番構成、default.json + 権限を絞ったoverlay.json
  • バリアントB — 寛容なケーパビリティ(現在ロードされている全プラグインの全allow-*を1つのdefault.jsonにまとめた構成)

src-tauri/src/lib.rsに計装を仕込む:

use std::sync::OnceLock;
use std::time::Instant;

static BOOT_START: OnceLock<Instant> = OnceLock::new();

pub fn run() {
    let _ = BOOT_START.set(Instant::now());

    tauri::Builder::default()
        .setup(|app| {
            let start = BOOT_START.get().unwrap();
            eprintln!("[boot] setup done: {} ms", start.elapsed().as_millis());

            let window = app.get_webview_window("main").unwrap();
            let start_for_event = *start;
            window.on_window_event(move |event| {
                if let tauri::WindowEvent::Focused(true) = event {
                    eprintln!("[boot] first render: {} ms", start_for_event.elapsed().as_millis());
                }
            });

            Ok(())
        })
        // ...
}

起動ごとに3つのタイムスタンプを記録する:setup donejs bootfirst render。各バリアントについてコールド起動を10回ずつ実行(プロセスを終了させ、間に2秒のクールダウンを入れる)。


3. 結果

バリアントA — 狭いケーパビリティ(10回の中央値):

メトリクス中央値最小最大レンジ
setup done207.5 ms19524247
js boot406.5 ms331468137
first render414 ms340477137

バリアントB — 寛容なケーパビリティ(10回の中央値):

メトリクス中央値最小最大レンジ
setup done218 ms19127786
js boot359.5 ms327485158
first render386 ms334497163

差分(B − A、中央値):

メトリクスΔ msΔ %方向
setup done+10.5+5.1%狭い方が速い
js boot−47−11.6%寛容な方が速い
first render−28−6.8%寛容な方が速い

2点気づくことがある。

1つ目、差分は3つのメトリクスで逆方向を指している。setup-doneは狭いケーパビリティの方が速く、js-bootとfirst-renderは寛容な方が速い。一貫した勝者はいない。

2つ目、こちらの方が決定的だ:1つのバリアント内での10回のレンジ(137〜163 ms)が、バリアント間の最大差分(47 ms)のおよそ3倍ある。「シグナル」がコールド起動のジッターというノイズフロアの中に埋もれている。

これはシグナルではなく、ノイズだ。同じケーパビリティで2回ビルドしても、同じ程度のばらつきが出るだろう。


4. なぜ仮説は外れていたか

ケーパビリティシステムがランタイムで実際に何をしているか、読み直してみた。

ケーパビリティはバンドル時のtree-shakingではない。__TAURI__ JavaScriptブリッジを小さくしない。起動時に登録されるIPCハンドラの数を減らさない。tauri::Builderはケーパビリティに関係なく全プラグインのハンドラを登録する — ケーパビリティ層はその上に乗っていて、個別のコマンド呼び出しをゲートする。

JS側のinvoke("plugin:fs|read_text_file", ...)がRustランタイムに届くと、ディスパッチャは現在のウィンドウのケーパビリティテーブルを参照して、ハンドラを実行するか権限エラーを返すかを決める。そのルックアップはO(1)。コストは呼び出しごとに払われ、起動時には払われない。しかもマイクロ秒オーダーだ。

つまりケーパビリティを狭めても、コールド起動にはほとんど影響しない。プラグインレジストリは同じサイズ。ブリッジも同じサイズ。JSバンドルもまだ同じ@tauri-apps/plugin-*モジュール群をインポートしている。

変わるのはアプリがランタイムで何を許可されているかであって、起動の速さではない。完全に別の軸だ。


5. ケーパビリティが実際に買ってくれるもの

これは起動最適化ではなく、セキュリティ境界だ。

価値は影響範囲の縮小にある。攻撃者がオーバーレイウィンドウにXSSを差し込めたとする — たとえば悪意のあるペイロードが貼り付けられ、それがHTMLとしてレンダリングされた、とか。そのときplugin:fs:write_text_fileを呼び出してボルトを抜き出そうとしても、ケーパビリティテーブルがそれを止める。オーバーレイのケーパビリティはその権限を渡していない。ランタイムが呼び出しを拒否する。

ウィンドウごとのケーパビリティがなければ、すべてのウィンドウがプラグインのフルサーフェスにアクセスでき、1つのウィンドウのXSSがあらゆるコマンドに届く。あれば、各ウィンドウはIPC層でサンドボックス化される。

これは起動の話ではない。「ユーザーが1時間使った後、何かが想定外に動き出した」ときの話だ。ケーパビリティはアプリを速くしない。アプリの一部が敵対的に動き出したときの最悪ケースを縮める。

価値はある。ただし、想定していたのとは違う理由で。


6. まとめ

このベンチマークがパフォーマンスの主張を裏付けてくれると期待していた。逆に潰された。それが結果だ。

Tauri v2のケーパビリティを扱う人に向けて、具体的に2つだけ:

  • コールド起動の予算をケーパビリティスコープに賭けない。 数字は動かない。最適化は他のところで — 遅延ウィンドウ生成、依存関係の整理、スプラッシュスクリーンによる目くらまし。
  • 「パフォーマンスのため」にケーパビリティを緩めない。 ノイズに紛れる程度の差分のために、本物のセキュリティの余裕を交換することになる。ケーパビリティを広げる正当な理由は機能(コマンドがより広いアクセスを必要とする)か保守性(マトリクスが追えなくなる)だけだ。

もう少し広いメタの話:パフォーマンスについて自信たっぷりに書かれた1段落は、30行のベンチマークより価値が低い。ベンチマークは午後があれば取れる。