| name | gpui-performance |
| description | Performance optimization techniques for GPUI including rendering optimization, layout performance, memory management, and profiling strategies. Use when user needs to optimize GPUI application performance or debug performance issues. |
GPUI Performance Optimization
Metadata
This skill provides comprehensive guidance on optimizing GPUI applications for rendering performance, memory efficiency, and overall runtime speed.
Instructions
Rendering Optimization
Understanding the Render Cycle
State Change → cx.notify() → Render → Layout → Paint → Display
Key Points:
- Only call
cx.notify() when state actually changes
- Minimize work in
render() method
- Cache expensive computations
- Reduce element count and nesting
Avoiding Unnecessary Renders
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();
Timer::after(Duration::from_millis(16)).await;
}
}).detach();
}
}
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();
}
}
}
Optimize Subscription Updates
let _subscription = cx.observe(&model, |_, _, cx| {
cx.notify();
});
let _subscription = cx.observe(&model, |this, model, cx| {
let data = model.read(cx);
if data.relevant_field != this.cached_field {
this.cached_field = data.relevant_field.clone();
cx.notify();
}
});
Memoization Pattern
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)>>,
}
impl MemoizedComponent {
fn expensive_computation(&self, cx: &ViewContext<Self>) -> String {
let data = self.model.read(cx);
let mut hasher = DefaultHasher::new();
data.relevant_fields.hash(&mut hasher);
let hash = hasher.finish();
if let Some((cached_hash, cached_result)) = &*self.cached_result.borrow() {
if *cached_hash == hash {
return cached_result.clone();
}
}
let result = perform_expensive_computation(&data);
*self.cached_result.borrow_mut() = Some((hash, result.clone()));
result
}
}
Layout Performance
Minimize Layout Complexity
div()
.flex()
.child(
div()
.flex()
.child(
div()
.flex()
.child(
div().child("Content")
)
)
)
div()
.flex()
.flex_col()
.gap_4()
.child("Header")
.child("Content")
.child("Footer")
Use Fixed Sizing When Possible
div()
.w(px(200.))
.h(px(100.))
.child("Fixed size")
div()
.w_full()
.h_full()
.child("Dynamic size")
Avoid Layout Thrashing
impl Render for BadComponent {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let width = cx.window_bounds().get_bounds().size.width;
div().w(width)
}
}
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();
}
}
}
Virtual Scrolling for Long Lists
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 {
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());
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())
})
)
)
)
}
}
Memory Management
Preventing Memory Leaks
impl BadView {
fn new(model: Model<Data>, cx: &mut ViewContext<Self>) -> Self {
cx.observe(&model, |_, _, cx| cx.notify());
Self { model }
}
}
struct GoodView {
model: Model<Data>,
_subscription: Subscription,
}
impl GoodView {
fn new(model: Model<Data>, cx: &mut ViewContext<Self>) -> Self {
let _subscription = cx.observe(&model, |_, _, cx| cx.notify());
Self { model, _subscription }
}
}
Avoid Circular References
struct CircularRef {
self_view: Option<View<Self>>,
}
struct NoCycle {
other_view: View<OtherView>,
}
Bounded Collections
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);
while self.items.len() > MAX_HISTORY {
self.items.pop_front();
}
}
}
Reuse Allocations
struct BufferedComponent {
buffer: String,
}
impl BufferedComponent {
fn format_data(&mut self, data: &[Item]) -> &str {
self.buffer.clear();
for item in data {
use std::fmt::Write;
write!(&mut self.buffer, "{}\n", item.name).ok();
}
&self.buffer
}
}
Profiling Strategies
CPU Profiling with cargo-flamegraph
cargo install flamegraph
cargo flamegraph --bin your-app
cargo flamegraph --bin your-app --features profiling
Memory Profiling
valgrind --tool=massif --massif-out-file=massif.out ./target/release/your-app
ms_print massif.out
heaptrack ./target/release/your-app
heaptrack_gui heaptrack.your-app.*.gz
instruments -t "Allocations" ./target/release/your-app
Custom Performance Monitoring
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();
}
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)]
}
}
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);
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(),
);
}
}
}
Benchmark with Criterion
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);
Batching Updates
for item in items {
self.model.update(cx, |model, cx| {
model.add_item(item);
cx.notify();
});
}
self.model.update(cx, |model, cx| {
for item in items {
model.add_item(item);
}
cx.notify();
});
Async Rendering Optimization
struct AsyncView {
loading_state: Model<LoadingState>,
}
impl AsyncView {
fn load_data(&mut self, cx: &mut ViewContext<Self>) {
let loading_state = self.loading_state.clone();
self.loading_state.update(cx, |state, cx| {
*state = LoadingState::Loading;
cx.notify();
});
cx.spawn(|_, mut cx| async move {
let data = fetch_data().await?;
cx.update_model(&loading_state, |state, cx| {
*state = LoadingState::Loaded(data);
cx.notify();
})?;
Ok::<_, anyhow::Error>(())
}).detach();
}
}
Caching Strategies
Result Caching
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);
}
}
Resources
Performance Targets
Rendering:
- Target: 60 FPS (16.67ms per frame)
- Render + Layout: ~10ms
- Paint: ~6ms
- Warning: Any frame > 16ms
Memory:
- Monitor heap growth
- Warning: Steady increase (leak)
- Target: Stable after initialization
Startup:
- Window display: < 100ms
- Fully interactive: < 500ms
Profiling Tools
CPU Profiling:
- cargo-flamegraph: Visualize CPU time
- perf (Linux): System-level profiling
- Instruments (macOS): Apple's profiler
Memory Profiling:
- valgrind/massif: Memory usage tracking
- heaptrack: Heap allocation tracking
- Instruments: Memory allocations
Benchmarking:
- criterion: Statistical benchmarking
- cargo bench: Built-in benchmarks
- hyperfine: Command-line tool benchmarking
Best Practices
- Measure First: Profile before optimizing
- Minimize Renders: Only
cx.notify() when necessary
- Cache Results: Memoize expensive computations
- Batch Updates: Group state changes
- Virtual Scrolling: For long lists
- Flat Layouts: Avoid deep nesting
- Fixed Sizing: When possible
- Monitor Memory: Watch for leaks
- Async Loading: Don't block UI
- Test Performance: Include benchmarks
Common Bottlenecks
- Subscription in render (memory leak)
- Expensive computation in render
- Deep component nesting
- Unnecessary rerenders
- Layout thrashing
- Large lists without virtualization
- Memory leaks from circular refs
- Unbounded collections