Rust GUI Architecture Models
Every Rust desktop GUI framework answers one foundational architecture question before anything else: does the UI get rebuilt from scratch every frame, or does it persist as a data structure that changes incrementally? That single decision - immediate mode versus retained mode - explains more about how egui, iced, and Tauri differ than any feature comparison of their widgets.
This page is the conceptual anchor for the Desktop GUI section. The basics page walks through code in each framework; this page explains why that code looks so different depending on which framework it comes from, so the deeper egui, iced, Tauri, and state-and-architecture pages make sense as variations on a shared theme rather than three unrelated APIs.
Summary
- Rust GUI frameworks split along an architecture axis - immediate mode rebuilds the UI every frame from current state, retained mode keeps a persistent widget tree and updates it in response to discrete events.
- Insight: This choice determines how you think about state, how testable your UI logic is, how the framework performs under load, and how much the UI feels "native" versus "tool-like."
- Key Concepts: immediate mode, retained mode, widget tree, Model-Update-View (Elm architecture), WebView, diffing.
- When to Use: Immediate mode for internal tools, debug overlays, and editors; retained/Elm-style for consumer-facing native apps; WebView-backed (Tauri) when a team already owns web UI skills or wants a small binary with a rich frontend ecosystem.
- Limitations/Trade-offs: Immediate mode trades some CPU efficiency and accessibility polish for simplicity; retained mode trades some architectural overhead for testability and native performance; WebView approaches trade a pure-Rust stack for web tooling and JS interop risk.
- Related Topics: reactive UI programming, the Elm architecture, native windowing (
winit), IPC security boundaries.
Foundations
Immediate-mode GUI (IMGUI) means the entire widget tree is reconstructed every single frame, directly from whatever your application state currently is. There is no persistent object graph of buttons and labels sitting in memory between frames; instead, your update function runs 60 times a second (or whenever a repaint is requested), and each run says "draw a button here, a slider there" based on the current values. egui is Rust's flagship immediate-mode library, paired with the eframe windowing integration.
Retained-mode GUI is the more traditional model familiar from desktop toolkits: the framework holds a persistent tree of widget objects, and your code mutates that tree (or a description of it) in response to events, letting the framework figure out what actually changed and repaint only the difference. iced implements this via the Elm architecture - a pure Model, an update function that turns messages into new model state, and a view function that turns the model into a widget description, which iced then diffs against the previous frame.
A third path sidesteps the immediate/retained distinction entirely. Tauri does not implement its own rendering model at all - it embeds the operating system's native WebView (WebView2 on Windows, WKWebView on macOS, WebKitGTK on Linux) and lets standard HTML, CSS, and JavaScript handle rendering, while Rust owns the backend: filesystem access, OS integration, and anything security-sensitive, communicated across an IPC boundary. Understanding these three shapes - rebuild-every-frame, persistent-tree-with-diffing, and delegate-to-the-browser-engine - is the single most useful mental model for choosing among Rust's desktop GUI options.
Mechanics & Interactions
In egui, a frame begins when the windowing layer (winit, wrapped by eframe) delivers input events and calls your App::update method with a mutable reference to your application struct and the current egui::Context. Inside that call, you issue imperative-feeling statements like ui.button("Save"), and egui internally tracks which widget IDs were touched this frame, computes layout, and paints. Because the whole tree is rebuilt every frame, there is no diffing step for your code to reason about - state lives directly on your struct, and the "UI" is just a projection of it computed fresh each time.
Retained-mode iced inverts that flow. Your view(&self) -> Element<Message> function is pure: given the current model, it returns a description of what the UI should look like, and iced's runtime diffs that description against the previous one to compute the minimal set of visual changes. User interaction does not mutate anything directly - it produces a Message value, which iced routes to your update(&mut self, message: Message) function, which is the only place state changes. This unidirectional loop (Model -> View -> Message -> Update -> Model) is deliberately reminiscent of the Elm language's architecture, and it is why iced code reads more like a reducer than an imperative UI script.
// iced: view is pure (model -> description), update is the only mutation point
fn update(count: &mut i32, message: Message) {
match message {
Message::Increment => *count += 1, // the ONLY place state changes
}
}Tauri's mechanics are different in kind, not just degree. The WebView owns the entire render loop using the browser engine's own layout and paint pipeline, which Rust never touches directly. Rust's role is to expose #[tauri::command] functions that the JavaScript frontend invokes over a serialized IPC channel, and to hold State<T> that survives across those invocations. This means the immediate/retained question is answered entirely on the JavaScript side (React's virtual DOM diffing, Svelte's compiled reactivity, or plain DOM manipulation) - Rust's architectural concern in Tauri is the IPC security boundary, not the render loop.
Advanced Considerations & Applications
Performance characteristics diverge sharply at scale. Immediate-mode egui recomputes layout for every visible widget every frame by design, which is cheap for dashboards and inspectors with a few hundred widgets but can become a real cost with deeply nested, thousand-widget screens - the fix is almost always to do expensive computation on a background thread and poll results via a channel, not to fight the frame loop itself. Retained-mode iced pays a diffing cost instead, which scales better with large, mostly-static UIs (a settings screen with rarely-changing widgets) but adds architectural overhead for genuinely fast-changing visualizations (a live waveform) where immediate mode's "just redraw everything" model is often simpler and just as fast in practice.
Accessibility is an underrated axis. Retained-mode toolkits and WebView-backed UIs generally have a much easier path to screen-reader support, because the underlying platform (native widget APIs, or the browser's accessibility tree) already understands persistent UI structure. Immediate-mode frameworks rebuild everything every frame, which makes exposing a stable accessibility tree to OS APIs harder - a genuine, honest trade-off, not a temporary gap.
State architecture also compounds differently as an app grows. In egui, state naturally accumulates on one large App struct unless you deliberately split it, because there is no framework-enforced boundary between "UI state" and "domain state." In iced, the Elm architecture forces that boundary from day one - your Model is your state, Message is your event vocabulary - which costs more upfront ceremony but pays off in testability, since update is a pure function you can unit test with plain values and no window at all.
| Approach | Strength | Weakness | Best Fit |
|---|---|---|---|
| Immediate mode (egui) | Simple mental model, fast iteration, easy background-thread integration | Recomputes every frame, weaker accessibility, state sprawl at scale | Internal tools, debug overlays, game engine inspectors |
| Retained/Elm (iced) | Testable pure update, diffing keeps large UIs efficient, enforced state boundaries | More upfront architecture, less flexible for rapidly-changing visualizations | Consumer-facing native apps, teams who like Redux-style unidirectional flow |
| WebView (Tauri) | Full web ecosystem for UI, small binary vs. Electron, Rust owns security-sensitive APIs | Render model lives outside Rust, IPC boundary must be carefully validated | Teams with strong web frontend skills wanting native packaging |
Common Misconceptions
- "Immediate mode means the UI is redrawn on the GPU every frame no matter what." - Not quite; egui still tracks whether input or state actually changed and can skip repainting when nothing requests it, "immediate" describes the widget code re-execution, not necessarily continuous GPU work.
- "Retained mode is always faster." - It is faster for mostly-static UIs, but the diffing step itself has a cost, and for UIs that change every frame anyway (live data, animations), immediate mode's simpler path can win.
- "Tauri is just Electron with extra steps." - The architecture is meaningfully different: Tauri uses the OS's existing WebView instead of bundling a Chromium runtime, which is the source of its much smaller binaries.
- "You have to pick one framework for your whole app." - Hybrid setups are common, most visibly
bevy_egui, which layers an immediate-mode debug UI on top of a game engine's own retained scene graph. - "Elm architecture is unique to iced." - It is a general pattern (unidirectional data flow, pure update functions) that shows up well beyond Rust, iced simply implements it natively for desktop GUIs.
FAQs
What's the one-sentence difference between immediate and retained mode?
Immediate mode rebuilds the entire widget description from state every frame; retained mode keeps a persistent widget tree and updates only what changed.
Why does egui call itself "immediate mode" if it still tracks widget IDs across frames?
It tracks IDs for things like focus and animation state, but your application code re-issues the full UI description every frame rather than mutating a persistent object graph.
Is iced's diffing similar to React's virtual DOM?
Conceptually yes - both compute a new UI description from state and diff it against the previous one to minimize actual rendering work.
Why is Tauri's binary smaller than Electron's?
Tauri embeds the operating system's already-installed WebView instead of bundling an entire Chromium runtime inside the app, which is where most of Electron's size comes from.
Can I test my UI logic without opening a window?
In iced, yes - update is a pure function of model and message, so you can call it directly in a unit test. In egui, it's harder because UI code runs inside the update(ctx, ...) callback tied to the frame loop.
Why do game engines favor immediate-mode UI for debug tools?
Debug overlays change constantly frame to frame anyway, so immediate mode's "just redraw it" model matches the workload, and bevy_egui integrates cleanly with an existing render loop.
Does immediate mode mean I can't have smooth animations?
No - egui supports animation and requests repaints when animating, but the animation state has to live somewhere explicit (often in Context memory) since there's no persistent widget object to hold it implicitly.
Is one architecture more "native-feeling" than the others?
Retained-mode toolkits with platform-specific widgets tend to feel most native; egui deliberately renders its own consistent look across platforms rather than using OS widgets; Tauri's feel depends entirely on the web UI built inside it.
Why does accessibility differ so much between these models?
Screen readers rely on a persistent, queryable UI structure, which retained-mode toolkits and the browser's accessibility tree already provide, while immediate-mode frameworks have to reconstruct and expose that structure every frame.
What happens to state architecture as an egui app grows?
State tends to accumulate on one large application struct unless you deliberately split it into modules, since egui doesn't enforce a state/UI boundary the way the Elm architecture does.
Is Tauri's IPC boundary a performance concern or a security concern?
Primarily security - every command is a trust boundary between untrusted frontend JavaScript and privileged Rust code, so input validation matters more here than raw IPC throughput for most apps.
Should I choose the framework based on 2D vs 3D, or based on this architecture question?
Based on this question first - rendering technology (wgpu, WebView) is often shared or swappable, but the immediate/retained/WebView split determines how your entire codebase is structured and tested.
Related
- GUI Basics - hands-on starter examples across all three frameworks
- egui - the immediate-mode framework in depth
- iced - the retained, Elm-style framework in depth
- Tauri - the WebView-backed architecture in depth
- State & Architecture - scaling state management within either model
Stack versions: This page is conceptual and not tied to a specific stack version.