gpui-performance について
GPUIアプリ開発者向けのパフォーマンス最適化ガイドで、描画、レイアウト、メモリ、実行効率を扱います。不要な再描画の抑制、計算結果のキャッシュ、仮想スクロール、プロファイリング方法を紹介します。
主な機能
- 01不要な再描画の削減
- 02レイアウトと仮想スクロールの最適化
- 03サブスクリプションとメモリの管理
- 04計算のキャッシュと更新の一括化
- 05プロファイリングとベンチマーク
活用シーン
- 01GPUIアプリの描画遅延を調査
- 02アプリのCPU・メモリ使用量を分析
- 03大規模リストや頻繁な状態更新を最適化
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ geoffjay/ claude-plugins --skill gpui-performanceインストール後、Claude Code で/gpui-performanceを入力して読み込まれたか確認します。
詳細
GPUI のパフォーマンス最適化
メタデータ
このスキルでは、GPUI アプリケーションの描画パフォーマンス、メモリ効率、全体的な実行速度を最適化するための包括的なガイダンスを提供します。
手順
描画の最適化
描画サイクルを理解する
State Change → cx.notify() → Render → Layout → Paint → Display
重要なポイント:
- 状態が実際に変化した場合にのみ
cx.notify()を呼び出します render()メソッド内の処理を最小限に抑えます- コストの高い計算結果をキャッシュします
- 要素数とネストを減らします
不要な再描画を避ける
// BAD: Renders on every frame
impl MyComponent {
fn start_animation(&mut self, cx: &mut ViewContext<Self>) {
cx.spawn(|this, mut cx| async move {
loop {
cx.update(|_, cx| cx.notify()).ok(); // Forces rerender!
Timer::after(Duration::from_millis(16)).await;
}
}).detach();
}
}
// GOOD: Only render when state changes
impl MyComponent {
fn update_value(&mut self, new_value: i32, cx: &mut ViewContext<Self>) {
if self.value != new_value {
self.value = new_value;
cx.notify(); // Only notify on actual change
}
}
}
サブスクリプションの更新を最適化する
// BAD: Always rerenders on model change
let _subscription = cx.observe(&model, |_, _, cx| {
cx.notify(); // Rerenders even if nothing relevant changed
});
// GOOD: Selective updates
let _subscription = cx.observe(&model, |this, model, cx| {
let data = model.read(cx);
// Only rerender if relevant field changed
if data.relevant_field != this.cached_field {
this.cached_field = data.relevant_field.clone();
cx.notify();
}
});
メモ化パターン
use std::cell::RefCell;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
struct MemoizedComponent {
model: Model<Data>,
cached_result: RefCell<Option<(u64, String)>>, // (hash, result)
}
impl MemoizedComponent {
fn expensive_computation(&self, cx: &ViewContext<Self>) -> String {
let data = self.model.read(cx);
// Calculate hash of input
let mut hasher = DefaultHasher::new();
data.relevant_fields.hash(&mut hasher);
let hash = hasher.finish();
// Return cached if unchanged
if let Some((cached_hash, cached_result)) = &*self.cached_result.borrow() {
if *cached_hash == hash {
return cached_result.clone();
}
}
// Compute and cache
let result = perform_expensive_computation(&data);
*self.cached_result.borrow_mut() = Some((hash, result.clone()));
result
}
}
レイアウトのパフォーマンス
レイアウトの複雑さを最小限に抑える
// BAD: Deep nesting
div()
.flex()
.child(
div()
.flex()
.child(
div()
.flex()
.child(
div().child("Content")
)
)
)
// GOOD: Flat structure
div()
.flex()
.flex_col()
.gap_4()
.child("Header")
.child("Content")
.child("Footer")
可能な場合は固定サイズを使用する
// BETTER: Fixed sizes (no layout calculation)
div()
.w(px(200.))
.h(px(100.))
.child("Fixed size")
// SLOWER: Dynamic sizing (requires layout calculation)
div()
.w_full()
.h_full()
.child("Dynamic size")
レイアウトスラッシングを避ける
// BAD: Reading layout during render
impl Render for BadComponent {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let width = cx.window_bounds().get_bounds().size.width;
// Using width immediately causes layout thrashing
div().w(width)
}
}
// GOOD: Cache layout-dependent values
struct GoodComponent {
cached_width: Pixels,
}
impl GoodComponent {
fn on_window_resize(&mut self, cx: &mut ViewContext<Self>) {
let width = cx.window_bounds().get_bounds().size.width;
if self.cached_width != width {
self.cached_width = width;
cx.notify();
}
}
}
長いリストには仮想スクロールを使用する
struct VirtualList {
items: Vec<String>,
scroll_offset: f32,
viewport_height: f32,
item_height: f32,
}
impl Render for VirtualList {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
// Calculate visible range
let start_index = (self.scroll_offset / self.item_height).floor() as usize;
let visible_count = (self.viewport_height / self.item_height).ceil() as usize;
let end_index = (start_index + visible_count).min(self.items.len());
// Only render visible items
div()
.h(px(self.viewport_height))
.overflow_y_scroll()
.on_scroll(cx.listener(|this, event, cx| {
this.scroll_offset = event.scroll_offset.y;
cx.notify();
}))
.child(
div()
.h(px(self.items.len() as f32 * self.item_height))
.child(
div()
.absolute()
.top(px(start_index as f32 * self.item_height))
.children(
self.items[start_index..end_index]
.iter()
.map(|item| {
div()
.h(px(self.item_height))
.child(item.as_str())
})
)
)
)
}
}
メモリ管理
メモリリークを防止する
// LEAK: Subscription not stored
impl BadView {
fn new(model: Model<Data>, cx: &mut ViewContext<Self>) -> Self {
cx.observe(&model, |_, _, cx| cx.notify()); // Leak!
Self { model }
}
}
// CORRECT: Store subscription
struct GoodView {
model: Model<Data>,
_subscription: Subscription, // Cleaned up on Drop
}
impl GoodView {
fn new(model: Model<Data>, cx: &mut ViewContext<Self>) -> Self {
let _subscription = cx.observe(&model, |_, _, cx| cx.notify());
Self { model, _subscription }
}
}
循環参照を避ける
// BAD: Circular reference
struct CircularRef {
self_view: Option<View<Self>>, // Circular!
}
// GOOD: Use weak references or redesign
struct NoCycle {
other_view: View<OtherView>, // No cycle
}
サイズ制限付きコレクション
use std::collections::VecDeque;
const MAX_HISTORY: usize = 100;
struct BoundedHistory {
items: VecDeque<Item>,
}
impl BoundedHistory {
fn add_item(&mut self, item: Item) {
self.items.push_back(item);
// Maintain size limit
while self.items.len() > MAX_HISTORY {
self.items.pop_front();
}
}
}
アロケーションを再利用する
struct BufferedComponent {
buffer: String, // Reused across operations
}
impl BufferedComponent {
fn format_data(&mut self, data: &[Item]) -> &str {
self.buffer.clear(); // Reuse allocation
for item in data {
use std::fmt::Write;
write!(&mut self.buffer, "{}\n", item.name).ok();
}
&self.buffer
}
}
プロファイリング戦略
cargo-flamegraph による CPU プロファイリング
# Install
cargo install flamegraph
# Profile application
cargo flamegraph --bin your-app
# With specific features
cargo flamegraph --bin your-app --features profiling
# Opens flamegraph.svg showing CPU time distribution
メモリプロファイリング
# valgrind (Linux)
valgrind --tool=massif --massif-out-file=massif.out ./target/release/your-app
ms_print massif.out
# heaptrack (Linux)
heaptrack ./target/release/your-app
heaptrack_gui heaptrack.your-app.*.gz
# Instruments (macOS)
instruments -t "Allocations" ./target/release/your-app
カスタムパフォーマンス監視
use std::time::Instant;
struct PerformanceMonitor {
frame_times: VecDeque<Duration>,
max_samples: usize,
}
impl PerformanceMonitor {
fn new() -> Self {
Self {
frame_times: VecDeque::with_capacity(100),
max_samples: 100,
}
}
fn record_frame(&mut self, duration: Duration) {
self.frame_times.push_back(duration);
if self.frame_times.len() > self.max_samples {
self.frame_times.pop_front();
}
// Warn if frame is slow (> 16ms for 60fps)
if duration.as_millis() > 16 {
eprintln!("⚠️ Slow frame: {}ms", duration.as_millis());
}
}
fn average_fps(&self) -> f64 {
if self.frame_times.is_empty() {
return 0.0;
}
let total: Duration = self.frame_times.iter().sum();
let avg = total / self.frame_times.len() as u32;
1000.0 / avg.as_millis() as f64
}
fn percentile(&self, p: f64) -> Duration {
let mut sorted: Vec<_> = self.frame_times.iter().copied().collect();
sorted.sort();
let index = (sorted.len() as f64 * p) as usize;
sorted[index.min(sorted.len() - 1)]
}
}
// Usage in component
impl MyView {
fn measure_render<F>(&mut self, f: F, cx: &mut ViewContext<Self>)
where
F: FnOnce(&mut Self, &mut ViewContext<Self>)
{
let start = Instant::now();
f(self, cx);
let elapsed = start.elapsed();
self.perf_monitor.record_frame(elapsed);
// Log stats periodically
if self.frame_count % 60 == 0 {
println!(
"Avg FPS: {:.1}, p95: {}ms, p99: {}ms",
self.perf_monitor.average_fps(),
self.perf_monitor.percentile(0.95).as_millis(),
self.perf_monitor.percentile(0.99).as_millis(),
);
}
}
}
Criterion でベンチマークを実施する
// benches/component_bench.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion, BenchmarkId};
fn render_benchmark(c: &mut Criterion) {
let mut group = c.benchmark_group("rendering");
for size in [10, 100, 1000].iter() {
group.bench_with_input(
BenchmarkId::from_parameter(size),
size,
|b, &size| {
b.iter(|| {
App::test(|cx| {
let items = vec![Item::default(); size];
let view = cx.new_view(|cx| {
ListView::new(items, cx)
});
view.update(cx, |view, cx| {
black_box(view.render(cx));
});
});
});
}
);
}
group.finish();
}
criterion_group!(benches, render_benchmark);
criterion_main!(benches);
更新の一括処理
// BAD: Multiple individual updates
for item in items {
self.model.update(cx, |model, cx| {
model.add_item(item); // Triggers rerender each time!
cx.notify();
});
}
// GOOD: Batch into single update
self.model.update(cx, |model, cx| {
for item in items {
model.add_item(item);
}
cx.notify(); // Single rerender
});
非同期描画の最適化
struct AsyncView {
loading_state: Model<LoadingState>,
}
impl AsyncView {
fn load_data(&mut self, cx: &mut ViewContext<Self>) {
let loading_state = self.loading_state.clone();
// Show loading immediately
self.loading_state.update(cx, |state, cx| {
*state = LoadingState::Loading;
cx.notify();
});
// Load asynchronously
cx.spawn(|_, mut cx| async move {
// Fetch data
let data = fetch_data().await?;
// Update state once
cx.update_model(&loading_state, |state, cx| {
*state = LoadingState::Loaded(data);
cx.notify();
})?;
Ok::<_, anyhow::Error>(())
}).detach();
}
}
キャッシュ戦略
結果のキャッシュ
use std::collections::HashMap;
struct CachedRenderer {
cache: RefCell<HashMap<String, CachedElement>>,
}
impl CachedRenderer {
fn render_cached(
&self,
key: String,
render_fn: impl FnOnce() -> AnyElement,
) -> AnyElement {
let mut cache = self.cache.borrow_mut();
cache.entry(key)
.or_insert_with(|| CachedElement::new(render_fn()))
.element
.clone()
}
fn invalidate(&self, key: &str) {
self.cache.borrow_mut().remove(key);
}
}
リソース
パフォーマンス目標
描画:
- 目標: 60 FPS(1 フレームあたり 16.67ms)
- 描画 + レイアウト: 約 10ms
- ペイント: 約 6ms
- 警告: 16ms を超えるフレーム
メモリ:
- ヒープの増加を監視します
- 警告: 継続的な増加(リーク)
- 目標: 初期化後は安定
起動:
- ウィンドウ表示: 100ms 未満
- 完全に操作可能になるまで: 500ms 未満
プロファイリングツール
CPU プロファイリング:
- cargo-flamegraph: CPU 時間を可視化します
- perf(Linux): システムレベルのプロファイリング
- Instruments(macOS): Apple のプロファイラー
メモリプロファイリング:
- valgrind/massif: メモリ使用量の追跡
- heaptrack: ヒープアロケーションの追跡
- Instruments: メモリアロケーション
ベンチマーク:
- criterion: 統計的ベンチマーク
- cargo bench: 組み込みのベンチマーク
- hyperfine: コマンドラインツールのベンチマーク
ベストプラクティス
- まず測定する: 最適化する前にプロファイリングします
- 再描画を最小限に抑える: 必要な場合にのみ
cx.notify()を呼び出します - 結果をキャッシュする: コストの高い計算をメモ化します
- 更新を一括処理する: 状態変更をまとめます
- 仮想スクロール: 長いリストに使用します
- フラットなレイアウト: 深いネストを避けます
- 固定サイズ: 可能な場合に使用します
- メモリを監視する: リークに注意します
- 非同期読み込み: UI をブロックしないようにします
- パフォーマンスをテストする: ベンチマークを含めます
よくあるボトルネック
- render 内のサブスクリプション(メモリリーク)
- render 内のコストの高い計算
- コンポーネントの深いネスト
- 不要な再描画
- レイアウトスラッシング
- 仮想化されていない大きなリスト
- 循環参照によるメモリリーク
- サイズ制限のないコレクション
コメント
Markdown 対応