9. パフォーマンス最適化とテスト戦略
GPUIのGPU描画パイプラインを知る
GPUI がなぜこれほど高速(120 FPS)に動作するのか、そしてどうすればその性能を落とさずに大規模なアプリを作れるのかを理解するために、1 フレーム(120 FPS なら約 8.3 ミリ秒)の裏側で起きている 3 つのフェーズ を見てみましょう。
graph LR
Phase1["1. Render フェーズ<br/>View から Element ツリーを構築"] --> Phase2["2. Layout & Prepaint フェーズ<br/>Taffy で座標・サイズを計算し<br/>当たり判定(Hitbox)を登録"]
Phase2 --> Phase3["3. Paint & GPU フェーズ<br/>四角形・文字・影をバッチ化し<br/>GPU シェーダーで一括描画"]
- Render フェーズ:
cx.notify()が呼ばれたビューのrender()メソッドが実行され、軽量なElementのツリーが作られます。 - Layout & Prepaint フェーズ:
Taffy エンジンが各要素の実際の幅・高さ・座標を計算し、マウスクリックを受け取るための当たり判定領域(
Hitbox)を配置します。 - Paint & GPU フェーズ: 背景の四角形(Quad)、角丸、影(Shadow)、文字のグリフ(テクスチャアトラス)、SVG パスといった描画命令(プリミティブ)を集め、種類ごとにまとめて GPU へ転送し、1 回のドローコールで一気に画面を塗り上げます。
つまり、GPU での描画そのものは極めて高速であり、アプリが重くなるとしたらその原因のほとんどは 「1. Render フェーズで不必要に巨大なツリーを毎回作り直している」 か 「メインスレッドで重い処理をしている」 のどちらかです。
120 FPSを維持するための状態分割と設計のコツ
アプリの規模が大きくなっても常に 120 FPS を維持するための、実践的な 4 つの設計ルールを紹介します。
▸ 1. 頻繁に更新される部分は独立した子 Entity(子ビュー)に分ける
GPUI の再描画は Entity(ビュー)単位 で行われます。
もし「1 秒間に 60 回変化するアニメーションやストップウォッチ」の変数を、画面全体のルートビュー(MainWindowView)に直接持たせて cx.notify() を呼ぶと、毎フレーム画面全体の何百個もの部品の render() が走り直してしまいます。
[悪い例] ルートビューがすべてを持つ
MainWindowView (タイマー更新で毎回全体を再構築!)
├── Sidebar
├── BigTable
└── TimerText
[良い例] 頻繁に変わる部分を独立した Entity に切り出す
MainWindowView (再構築されない)
├── Entity<SidebarView>
├── Entity<BigTableView>
└── Entity<TimerView> (ここだけが cx.notify() で再描画される!)
頻繁に状態が変わる部品は、cx.new(|_| TimerView::new()) のように独立した Entity として切り出しましょう。
そうすれば、その子ビューの中で cx.notify() を呼んでも、親ビューや兄弟ビューの render() は一切実行されず、再計算コストを最小限に抑えられます。
▸ 2. 文字列やリストの受け渡しには SharedString と Arc を使う
render() メソッドは高頻度で呼ばれます。
その中で毎回大きな String や Vec を .clone() すると、メモリの確保とコピーが繰り返されてしまいます。
文字列には GPUI 標準の SharedString を使い、大きな読み取り専用データの共有には Arc<[T]> を使うことで、ポインタのコピーだけで瞬時に受け渡せます。
▸ 3. 100 件を超える一覧には必ず仮想化(VirtualList / Table)を使う
第 7 章で解説したとおり、スクロール領域の中に何百件もの要素を並べる場合は、通常の v_flex().children(...) ではなく、必ず GPUI の uniform_list や GPUI-Component の List / Table を使ってください。
▸ 4. render() メソッドの中でファイルの読み書きや通信をしない
render() メソッドは「現在のメモリの状態をそのまま画面要素に写し取るだけの純粋な処理」に徹する必要があります。
データの検索やソート、ファイルアクセスなどは、イベントハンドラや cx.background_executor() 側であらかじめ済ませておき、結果をビューのフィールドに保存してから cx.notify() を呼ぶように設計しましょう。
ヘッドレスUIテスト(#[gpui::test])の書き方
GUI アプリケーションの開発では、「ボタンを押したら本当に状態が変わるか」「ショートカットキーが正しく動くか」を自動テストで確認したい場面が多々あります。 しかし、テストのたびに本物の OS ウィンドウを開いていたのでは、動作が遅いうえに CI(GitHub Actions などの自動検証サーバー)上で動かすのが困難です。
GPUI には、実際のウィンドウを画面に出さずにメモリ上だけでウィンドウとイベントを完全に再現する ヘッドレス UI テスト機構(#[gpui::test]) が最初から組み込まれています。
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
use gpui::TestAppContext;
// #[gpui::test] を付けると、テスト用の仮想環境(TestAppContext)が自動で注入される
#[gpui::test]
async fn test_counter_increment(cx: &mut TestAppContext) {
// 1. 仮想ウィンドウを開き、テスト対象のビューを作成する
let (view, cx) = cx.add_window_view(|_window, _cx| CounterView { count: 0 });
// 初期状態が 0 であることを確認する
view.read_with(cx, |counter, _cx| {
assert_eq!(counter.count, 0);
});
// 2. キー入力やアクション、状態更新をシミュレートする
view.update(cx, |counter, cx| {
counter.count += 1;
cx.notify();
});
// 非同期タスクや再描画予約がすべて落ち着くまで進める
cx.run_until_parked();
// 3. 更新後の状態を検証する
view.read_with(cx, |counter, _cx| {
assert_eq!(counter.count, 1);
});
}
}
}
#[gpui::test] を使うと、以下のような高度なテストも通常の cargo test コマンドで一瞬で実行できます。
cx.simulate_keystrokes("cmd-n hello enter"): 実際のキーボード入力を順番に再現する。cx.simulate_click(position, Modifiers::none()): 指定座標へのマウスクリックを再現する。cx.executor().advance_clock(Duration::from_secs(5)): 実際には 5 秒待たずに、仮想時計を 5 秒進めてタイマー処理を即座にテストする。- ランダムシードを使った並列実行テスト(非同期処理のタイミングによるバグを自動検出する機能)。
アクセシビリティ(AccessKit)と多言語対応
実用的なデスクトップアプリとして広く使われるためには、アクセシビリティや多言語(国際化)への配慮も欠かせません。
▸ アクセシビリティとキーボードナビゲーション
GPUI および GPUI-Component は、Rust 製のクロスプラットフォームなアクセシビリティ基盤である AccessKit と連携し、スクリーンリーダーへの情報提供やキーボードだけでの画面操作をサポートしています。
GPUI-Component の Button、Input、Checkbox、Modal などは、Tab キーによるフォーカス移動(タブストップ)や矢印キーでのリスト選択、Esc キーでのキャンセル操作が標準で組み込まれています。
▸ 多言語対応(i18n)とフォント設定
GPUI-Component の内部メッセージ(たとえばテーブルの空表示メッセージやダイアログのボタン名など)はロケール設定に対応しており、アプリの言語設定に合わせて切り替えられます。
また、日本語環境で文字化けや不自然な中華フォント(CJK 統合漢字の字形問題)を防ぐため、Theme::global_mut(cx).font_family に OS 標準の日本語フォント(Windows なら "Yu Gothic UI" や "Meiryo"、macOS なら "Hiragino Sans")を指定しておくと、どの環境でも美しい日本語表示が得られます。
アプリケーションのビルドと配布
開発中のデバッグビルド(cargo run)では、コンパイル速度を優先して最適化が省かれているため、実行ファイルが大きくなり、描画パフォーマンスも本来の力を発揮していません。
ユーザーに配布するときや、本来の 120 FPS の描画性能を計測するときは、必ず --release フラグ を付けてビルドします。
cargo build --release
さらに、Cargo.toml に以下のようなリリースプロファイル設定を追加しておくと、最終的な実行ファイルのサイズを大幅に縮小し、実行速度を極限まで高めることができます。
[profile.release]
opt-level = 3 # 最大限の速度最適化を行う
lto = "thin" # リンク時最適化(Link Time Optimization)を有効にする
codegen-units = 1 # クレート全体の最適化効率を高める
strip = true # デバッグシンボルを除去してファイルサイズを小さくする
また、Windows 環境でアプリをダブルクリックして起動したときに、背後に黒いコマンドプロンプト(コンソール画面)が表示されないようにするには、src/main.rs の一番上の行に以下の属性を 1 行追加します。
#![allow(unused)]
fn main() {
// リリースビルド時のみ、Windowsのコンソールウィンドウを非表示にする
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
}
できあがった実行ファイルは、macOS なら .app バンドル(cargo-bundle クレートなどを利用)、Windows なら MSI / NSIS インストーラー(cargo-packager や wix などを利用)、Linux なら AppImage や Flatpak、Debian パッケージとしてパッケージングすることで、ユーザーが簡単にインストールできる製品として配布できます。
本書を通じて、GPUI の基礎概念である状態管理(Entity と Context)、宣言的なレイアウトとスタイリング、イベントと非同期処理から、GPUI-Component を活用したテーマ設計、フォーム制御、仮想化テーブル、ドックレイアウト、そして実践的なアプリケーション構築までを一通り学びました。
Rust の安全性と GPU の圧倒的な描画性能を兼ね備えた GPUI エコシステムを活用して、ぜひあなた自身の快適で高速なデスクトップアプリケーションを開発してみてください。