Performance Considerations
Building performant Terminal User Interfaces (TUIs) requires careful attention, as direct terminal manipulation can be slower than native graphical UIs. OSUI is designed with performance in mind, offering mechanisms to optimize rendering and reactivity.
1. DynWidget vs. StaticWidget
This is perhaps the most crucial performance decision in OSUI.
-
StaticWidget:- Creation: The
Elementinstance is created only once when the widget is initially loaded. - Rendering: During each
Screen::render()cycle, theElement::renderandElement::after_rendermethods are called directly on the existingElementinstance. TheElement's internal state (if any) is mutated directly. - Overhead: Minimal. No re-allocation or re-evaluation of closures per frame.
- When to Use: For any part of your UI that does not change its fundamental structure or the type of its root
Elementinstance. This includes static text, fixed layouts, or elements whose internal state changes but doesn't require a full rebuild of theElementitself. - In
rsx!: Use thestatickeyword:static Div { "Hello" }.
- Creation: The
-
DynWidget:- Creation: Holds a closure (
FnMut() -> WidgetLoad) that rebuilds theElementand its initial components wheneverrefresh()is called. - Rendering: During
Screen::render(), ifauto_refresh()determines that a dependency has changed, the widget's internalElementis entirely replaced by re-executing the creation closure. Then, therendermethods are called on this newElementinstance. - Overhead: Higher. Involves re-allocations for the new
ElementandHashMapof components, plus the cost of re-evaluating the closure and potentially re-parsing text for elements likeformat!()strings. - When to Use: For parts of your UI that must change their
Elementtype, or whose content is deeply tied to reactiveStatethat necessitates a full rebuild to reflect changes. Use it for dynamic text, lists that grow/shrink, or components that switch between different visual representations. - In
rsx!: Default behavior or using%dependency:%my_state Div { "Count: {my_state}" }.
- Creation: Holds a closure (
Recommendation: Favor static widgets whenever possible. Break down your UI into the smallest possible DynWidgets to isolate reactive updates and minimize the scope of re-renders.
2. State<T> Usage and Granularity
OSUI's State<T> is efficient for simple values, but consider its implications for larger data structures.
- Modification Cost: When
**my_state.get() = ...ormy_state.set(...)is called, it marksinner.changed = inner.dependencies. This means allDynWidgets listening to that specificState<T>will be rebuilt on the nextauto_refreshcycle. - Large
StateObjects: IfTinState<T>is a large struct orVec, and you only modify a small part of it, the entireDynWidget(and its children) listening to it will still rebuild. - Optimization:
- Splitting State: If a complex data structure has independent parts that change, consider splitting it into multiple
Stateobjects.// Instead of:
struct UserProfile { name: String, email: String, settings: Settings }
let profile = use_state(UserProfile { /* ... */ });
// And updating `profile.get().name = ...` which rebuilds everything.
// Consider:
let user_name = use_state(String::new());
let user_email = use_state(String::new());
let user_settings = use_state(Settings::new());
// Then, only widgets depending on `user_name` re-render when `user_name` changes. - Smart
ElementImplementations: For complex data, a customElementcan internally manage its ownStateand handle partial updates without requiring a full rebuild of theElementitself. For instance, anElementcould hold aState<Vec<Item>>and only re-render the changedItems internally, or update specificWidgetchildren based on diffing logic, rather than relying onDynWidgetto rebuild the entireElement.
- Splitting State: If a complex data structure has independent parts that change, consider splitting it into multiple
3. Terminal I/O Overhead
Every character printed to the terminal, especially with color or cursor positioning, incurs overhead due to ANSI escape code processing and actual screen updates by the terminal emulator.
- Full Screen Clear:
utils::clear()(used byScreen::render) clears the entire screen. This is a common TUI practice to avoid artifacts but is also a performance bottleneck for very high frame rates or remote connections. OSUI currently performs a full clear every frame. - Minimize Redraws: OSUI's reactive system already helps minimize what is rebuilt, but the
RenderScopethen draws the entire content of each widget. Terminal emulators often optimize partial updates, but minimizing the total area of change is always beneficial. - Batching:
RenderScopeimplicitly batches drawing commands beforedraw()is called. Avoid manual, unbufferedprint!calls in tight loops.
4. Expensive Operations in Element::render or FnMut() -> WidgetLoad Closures
-
Avoid Heavy Computation: Do not perform computationally intensive tasks (e.g., complex data processing, network requests, large file I/O) directly within
Element::renderor theFnMut() -> WidgetLoadclosure of aDynWidget. These are called frequently (every frame forrender, or every dependency change for the closure). -
Offload: If such operations are necessary, offload them to separate
std::thread::spawnthreads or use asynchronous runtime if your application supports it. UpdateState<T>from these background threads, and your UI will react.// BAD (expensive in render/build closure):
// rsx! { Div { format!("Result: {}", expensive_computation()) } }
// GOOD:
let computation_result = use_state("Calculating...".to_string());
std::thread::spawn({
let computation_result = computation_result.clone();
move || {
let result = expensive_computation(); // Runs in background
computation_result.set(format!("Result: {}", result)); // Updates state, triggers UI refresh
}
});
rsx! {
%computation_result
Div { "{computation_result}" }
}
5. Mutex Contention
OSUI uses Mutexes extensively (Arc<Mutex<T>>) for thread-safe access to widgets, elements, components, and state.
- Minimize Lock Duration: When you call
my_state.get()orwidget.get_elem(), you acquire aMutexGuard. Keep the duration for which you hold this lock as short as possible. Perform your read/write operation, thendropthe guard or let it go out of scope quickly. - Avoid Nested Locks: Do not acquire a
Mutexlock and then, while holding it, try to acquire anotherMutexthat could be held by a different thread trying to acquire your first lock. This leads to deadlocks.State::get_dl()is useful for avoiding this, as it releases the lock immediately after cloning.
By being mindful of these performance considerations, you can ensure your OSUI applications are responsive and efficient, even when handling complex UIs or frequent updates.