From rust-gpui-developer
Optimizes GPUI application performance with techniques for rendering, layout, memory management, and profiling. Use when debugging performance issues.
How this skill is triggered — by the user, by Claude, or both
Slash command
/rust-gpui-developer:gpui-performanceThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill provides comprehensive guidance on optimizing GPUI applications for rendering performance, memory efficiency, and overall runtime speed.
This skill provides comprehensive guidance on optimizing GPUI applications for rendering performance, memory efficiency, and overall runtime speed.
State Change → cx.notify() → Render → Layout → Paint → Display
Key Points:
cx.notify() when state actually changesrender() method// 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
}
}
# 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(),
);
}
}
}
// 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);
}
}
Rendering:
Memory:
Startup:
CPU Profiling:
Memory Profiling:
Benchmarking:
cx.notify() when necessarynpx claudepluginhub p/geoffjay-rust-gpui-developer-plugins-rust-gpui-developerGuides completion of development work by verifying tests, detecting environment, and presenting structured options for merge, PR, or cleanup.
Enforces test-driven development: write failing test first, then minimal code to pass. Use when implementing features or bugfixes.
Guides creation and editing of skills using test-driven development with pressure scenarios and subagents to verify agent compliance.