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.
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 necessarySearch for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer