Tauriのケーパビリティはコールド起動を速くする — と思っていた。違った。
仮説があった:Tauri v2のケーパビリティを最小限に絞れば、コールド起動が速くなるはずだ。注入される__TAURI__ブリッジが小さくなり、パースが減り、初期化のコストが下がり、最初の描画が速くなる。
筋は通っているように見えた。ベンチマークを取った。仮説は死んだ。
この記事は、その代わりに見つけたものについての話だ。
1. 仮説
Tauri v2では、ケーパビリティ宣言がsrc-tauri/capabilities/配下のウィンドウごとのJSONファイルに分割された。「メインウィンドウはcore:defaultとplugin: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 done、js boot、first render。各バリアントについてコールド起動を10回ずつ実行(プロセスを終了させ、間に2秒のクールダウンを入れる)。
3. 結果
バリアントA — 狭いケーパビリティ(10回の中央値):
| メトリクス | 中央値 | 最小 | 最大 | レンジ |
|---|---|---|---|---|
| setup done | 207.5 ms | 195 | 242 | 47 |
| js boot | 406.5 ms | 331 | 468 | 137 |
| first render | 414 ms | 340 | 477 | 137 |
バリアントB — 寛容なケーパビリティ(10回の中央値):
| メトリクス | 中央値 | 最小 | 最大 | レンジ |
|---|---|---|---|---|
| setup done | 218 ms | 191 | 277 | 86 |
| js boot | 359.5 ms | 327 | 485 | 158 |
| first render | 386 ms | 334 | 497 | 163 |
差分(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行のベンチマークより価値が低い。ベンチマークは午後があれば取れる。