← 記事一覧へ

プリウォーム済みの隠しWebViewは、コールド起動ごとに約125 ms余分にかかる

数週間前、Tauriウィンドウのプリウォームについて書いた — ホットキーで呼び出されるポップアップを「即座」に感じさせるためのテクニックだ。手法は機能する:ユーザーがウィンドウを閉じても破棄しない、隠す。WebViewはメモリ内で生き続ける。再表示はマイクロ秒オーダーの操作だ。

これはウォーム呼び出しには正しい選択だ。

コールド起動には間違っている — 少なくとも複数のウィンドウで同時にやる場合は。

この記事は、それがどのくらい悪いかを測った話だ。


1. セットアップ

Kurippatauri.conf.jsonで3つのTauriウィンドウを宣言している:

  • main — メインのクリップボードポップアップ
  • settings — 設定画面
  • activation — ライセンスのアクティベーション画面

3つすべてが起動時に作られ、3つすべてがプリウォームされていた(visible: false、いつでも表示できる状態)。理由はそれなりに筋が通っていた:起動後にユーザーが最初に触るのはどのウィンドウかわからない。どのインタラクションも即座にしたかった。

問題は、「3つのウィンドウをプリウォームする」ことがTauriの起動パイプラインで実際に何を意味するかだ。

各ウィンドウは独自のWebKit WebContentプロセスを起動する。それぞれが独自にV8を初期化する。それぞれが独自にIPCハンドラを登録する。それぞれが独自にReactをマウントする(KurippaのフロントエンドはTauri上のReactだ)。そしてそのすべてが、ユーザーが何かを目にする前に、メインスレッドで起きるか、メインスレッドの取り合いになる。

仮説:起動時のwindows配列から2つの補助ウィンドウを外し、初回利用時に遅延生成するようにすれば、メインウィンドウのコールド起動が速くなるはずだ。

ベンチマークを取った。


2. ベンチマーク

ケーパビリティのベンチマークと同じ計装:

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());
            // ... "[boot] main visible" を初回フォーカスでログ
            Ok(())
        })
        // ...
}

2種類のビルド:

  • ベースライン — 本番の構成。3つのウィンドウすべてをプリウォーム。
  • 遅延mainのみを起動時の配列に入れる。settingsactivationはRustのヘルパー経由で、既存のトレイ/メニューアクションから呼ばれたときに生成。

各バリアントについて10回のコールド起動。間にプロセスを終了させ、2秒のクールダウン。

遅延ヘルパーは単純だ:

fn ensure_window(app: &tauri::AppHandle, label: &str) -> tauri::Result<tauri::WebviewWindow> {
    if let Some(w) = app.get_webview_window(label) {
        return Ok(w);
    }
    tauri::WebviewWindowBuilder::new(
        app,
        label,
        tauri::WebviewUrl::App("/".into()),
    )
    .visible(false)
    .build()
}

設定ウィンドウを必要とする既存のコマンド — open_settings、トレイメニューの「Settings…」 — は、表示前にensure_window(&app, "settings")を呼ぶ。初回呼び出しでビルドし、以降はキャッシュにヒットする。


3. 結果

メトリクスベースライン(中央値)遅延(中央値)Δ msΔ %
setup done244 ms185 ms−59−24%
React mount469 ms344 ms−125−27%
最大(テール)913 ms500 ms−413−45%

中央値の改善は本物で、方向も一貫している(setup-doneとReact mountは同じ方向に動いた)。だが一番面白いのはテールレイテンシの結果だ。

ベースラインのコールド起動の最悪ケースは913 msだった。GPUの混み具合、ファイルシステムのジッター、メインスレッドを取り合う3つのWebView — 何かの不運な組み合わせで、中央値の倍近く遅い起動が時々起きていた。遅延構成では500 msに収まる。同じマシン、同じ条件、ただクリティカルパス上のWebViewが減っただけ。

中央値の差分よりも、ばらつきの収束の方が大事だ。125 msの中央値節約は嬉しい。413 msのテール短縮は最悪のユーザー体験が劇的に改善したことを意味する — 記憶に残るのはたいてい最悪の体験だ。


4. なぜ効くのか

「3つのウィンドウをプリウォームすればいい」と思っていたメンタルモデルは、「各ウィンドウのセットアップは並列に走る」というものだった。WebKitはウィンドウごとに別のWebContentプロセスを起動するから、互いをブロックしないはず、と。

これは半分本当で、半分間違っている。

確かにそうな点:各WebViewは独自のWebContentプロセスを持つ。各プロセス内でのV8初期化は、そのプロセスのメインスレッドで走り、他のプロセスから隔離されている。

そうでない点:TauriのRust側 — ウィンドウが配線され、IPCハンドラが登録される場所 — はsetupクロージャの中でシングルスレッドだ。そしてmacOSのウィンドウサーバーとメインスレッドのレイアウトパイプラインは並列ではない — NSWindowの生成、最初の描画パス、イベントループのブートストラップは、すべて同じメインスレッドを取り合う。

つまり「3つのウィンドウをプリウォーム」というのは、Rustのsetup内でWebviewWindowBuilder::new(...).build()が3回直列に走り、macOSウィンドウサーバーに3回NSWindow生成のリクエストが届き、3回の初期レイアウトパスが走るということだ。メインウィンドウの最初の描画は、そのキューの末尾に並ぶ。

2つ取り除けば、起動キューの3分の2が消える。メインウィンドウの最初の描画が前に出てくる。


5. トレードオフ

遅延生成のコスト:ユーザーが初めて設定を開いたとき、WebViewがゼロから作られる間に約250〜400 ms待つ。2回目以降は即座だ(ウィンドウはキャッシュされる)。

Kurippaにとってはこれが正しいトレードオフだ。なぜならほとんどのユーザーは設定を開かないし、開くユーザーもめったに開かない — 初期セットアップで1回、たまに設定をいじるくらい。ホットキーで呼び出されるメインインタラクションは1日に何十回も起きる。「初回の設定オープンが少し遅い」と「すべてのコールド起動が速い」を交換するのは、明確に得な選択だ。

逆のトレードオフが正しいのは、起動直後にユーザーが複数のウィンドウを頻繁に行き来することが期待される場合だ。その場合はプリウォームに意味があり、コールド起動のコストが入場料になる。

勝ちの形はアプリ依存だ。メカニズム — プリウォームされた隠しWebViewがメインウィンドウの起動と取り合う — は一般的だ。


6. 同じコインの裏表

以前のプリウォーム記事は「hide/showサイクルでウィンドウを生かし続ける」ことを支持した。この記事は「起動時に複数のウィンドウを作る」ことに反対している。矛盾しているように見えるが、矛盾していない。

  • ウォーム呼び出し(ホットキー → 既存の隠しウィンドウ → 表示):プリウォームは必須だ。なければホットキーを押すたびに完全なコールド起動コストを払うことになる。
  • コールド起動(プロセス起動 → 最初の描画):複数のウィンドウのプリウォームは税金だ。プリウォームされた各ウィンドウが、メインウィンドウが描画できるようになるまでにsetupの取り合いを増やす。

2つを折り合わせるルール:プリウォームは1つのウィンドウだけ — ユーザーが最初に見る可能性が一番高いもの。他はすべて遅延生成する。一度補助ウィンドウが作られたら、プロセスのライフタイムを通じてウォームのままだ(メインウィンドウと同じく、hide-on-closeで)。

Kurippaにとってはメインのクリップボードポップアップだ。あなたのアプリにとっては、支配的なインタラクションが何かによる。


7. まとめ

tauri.conf.jsonはアプリが必要としそうなウィンドウをすべて宣言できるようにしているが、それは罠だ。プリウォームされた各ウィンドウは他のウィンドウと、そしてメインウィンドウと、コールド起動の間ずっと取り合いをしている。ウォーム呼び出しで節約したパフォーマンスは、ユーザーが初めてアプリを起動したときに利息付きで返ってくる。

押さえておく価値のある3つの数字:

  • setup-done −24%
  • React mount −27%
  • テールレイテンシ −45%

午後1回の作業と、ensure_windowヘルパーへの小さなリファクタで。