Architecture10 min read
From 4 GB to 117 MB: Rust on the desktop
Native pays off for an all-day app: our Rust window used 117 MB in one process, against about 4 GB for the web view. Prove the hard parts first.
Mercury Labs
Native pays off for an all-day app: our Rust window used 117 MB in one process, against about 4 GB for the web view. Prove the hard parts first.
A native client drawn on the GPU is worth it when people keep the app open all day, when memory and idle CPU matter, and when you already have a public API and a design system it can be generated from. Our prototype of the Company Twin's desktop window, written in Rust on the Rune framework, used 117 MB in one process at rest. The web-view client it would replace used about 4 GB across 15 processes. That is not a like-for-like comparison: the prototype had no mail or calendar, which the web-view client includes. The framework is pre-release, so we proved the hardest parts first and kept it out of every crate that holds state.
A GPU-drawn native client renders its own interface (text, controls, lists and icons) directly on the graphics card from application code, with no embedded browser engine, HTML or CSS between the app and the screen.
This article explains why we started the native client, the five proofs we ran before committing to a framework that was weeks old, how the code is divided so that the risk stays contained, and what we have measured so far. It is pre-release software: reading the Twin works against our staging organisation, writing has started, and mail and calendar are still to come.
Our first desktop client, Twin Desk, is a sensible design. It wraps the Twin's web app in WebKit tabs and builds mail and calendar natively beside it, so every feature of the web app arrives on the desktop for free. On a working Mac its window used about 4 GB of memory across the app and 14 helper processes. Each web view brings its own content, networking and GPU processes, and an app that people leave open from morning to evening pays for that all day.
The trade-off is the usual one. A web view gives you every feature the web app has; a native client gives you control of memory, start-up, idle cost and offline behaviour, at the price of rebuilding what you show. We would not make that trade for an app opened occasionally. We made it for the client our own team uses for most of the day, which is also where mail, calendar and the Twin meet.
A prototype of the same window drawn by Rune, a GPU-rendered Rust UI framework, used 117 MB in a single process, and 0% CPU when nothing was happening. Rune is open source and very new: it was created in late October 2026, has no web view, HTML or CSS, and its authors take issues rather than pull requests. That combination of a large gain and a young framework shaped everything that followed.
By proving the parts most likely to fail before deciding, with throwaway code and measured results. We chose the five things a Twin client cannot do without and that a young framework is most likely to get wrong. Frame times below are CPU time per frame at 120 frames a second on an optimised build, run headless on Apple silicon; they exclude uploading to the GPU and presenting.
| Proof | What it covered | Result |
|---|---|---|
| Text input | A composer with @-mentions, mail compose, a record form, input methods, undo and accessibility | 8 headless checks pass. Typing Japanese or Chinese into a real window still needs a person at a keyboard |
| Streaming | A live agent session with reconnect and resume | All six scenarios of the session fixtures; resumes over a real socket; p95 0.32 ms per frame |
| Large channels | 10,000 records in table, board, list, calendar, timeline and matrix views, with 40 changes a second | Table p95 3.4 ms, board 2.4 ms, list 3.2 ms |
| Design system | All 21 themes and the web app's icon set | 546 sampled pixels within 0.9/255 of the browser's colours; 230 of 233 icons match a reference renderer |
| HTML mail | Rendering email without a web view and without fetching anything | On 25 made-up messages, 22 drew as intended, 2 simplified and 1 needed "open original". Real mail not yet measured |
Each proof used fixtures or made-up data; real-data results come from the milestones that followed.
The proofs also found problems. Two Rune drawing defects came out of the icon comparison: a round-capped line of almost zero length, which is how the Lucide icon set draws a dot, was dropped; and a round join was missing where a path turns back on itself. Until Rune fixes them, our icon generator writes those marks in a form Rune draws correctly, and reports for the framework's maintainers are drafted. That is the kind of finding you want before a decision, not after.
The client is its own Cargo workspace with its toolchain pinned. Rune is pinned to one git revision and bumped deliberately, with golden renders of every screen as the check. Every third-party crate is pinned to an exact version, unsafe code is denied, and so are unwrap, todo! and debug macros.
Only app, the composition root, and ui, the screens and controls, may depend on Rune. The tokens crate, which holds the generated themes, measures, motion and icons, may use its types only. Everything else is plain Rust: the Twin's state (the feed follower, projections, actions, optimistic changes and undo), the SQLite stores, the mail and calendar adapters, the updater, the platform layer and the gateway client. A test over cargo metadata fails if any of those gains a Rune dependency, or if any crate other than platform gains a platform-only one.
Three other choices keep the client honest about what it is.
The second milestone ran the app against our staging organisation through the public gateway on 11 November 2026. Sign-in goes through the default browser; the ID token's signature is verified and the sign-in is kept in the system keychain and nowhere else. A cold launch made 91 gateway reads, all of which succeeded. A relaunch made 5: everything else was drawn from the local store and the feed was followed on from the kept cursor. Switching organisation gives each its own store, and a session asked from the home screen was created and followed live as the agent answered.
| Measure | Result | Budget |
|---|---|---|
| Kept Twin ready after launch | 46 ms (48 to 75 ms in the earlier debug-build run) | Content on screen under 300 ms |
| Tour of a table, a board, the inbox and the sessions list | p95 2.1 ms per frame, mean 0.8 ms | p95 under 4 ms |
| An answer streaming into a session | p95 2.5 ms per frame, mean 1.1 ms | p95 under 4 ms |
| A real window at rest (debug build) | 0.0% CPU, 132 MB | 0% CPU; under 250 MB with three mail accounts |
Not yet measured: a 10,000-record channel on real data, memory with mail accounts connected, and the delay before a change made in the web app appears here.
Since then the app has shown a channel's table, list and board views from real records, marked an inbox item done and put it back with undo, sent a follow-up in a session and stopped a running turn. Self-updating has been built and proved between two signed builds on one Mac; it is not yet notarised or published.
The Mac comes first, but the code is written so that Windows does not need a rewrite. Platform code sits behind nine interfaces in the platform crate (secrets, code-signature checks, updates and packaging, data folders, launch at login, opening links, notifications, single instance and menus). No other crate may contain cfg(target_os), and a source test fails if one does. A missing Windows implementation fails loudly with a named error rather than falling back silently. Shortcuts are defined once on the primary modifier, every command lives in one action registry that the menu bar and command palette both read, and every change compiles for a Windows target in our pre-release checks, for the crates that need no C compiler. Nothing has shipped to Windows.
When the app is opened occasionally, a web view's memory is a fair price for having every feature at once. When most of what you show is HTML you do not control, such as rich documents or interactive canvases, a native client has to hand those to a browser anyway, as ours does for canvases and admin pages. And when there is no stable public API, the native client ends up coupled to private routes that change under it. Our client was viable because the gateway, the design system and the event feed already existed as contracts it could be generated from and tested against. We describe how each interface reaches the Twin in one Twin, many interfaces.
If you are weighing Electron, a system web view or native for an internal tool, we would start by measuring the current client where people actually use it: memory and processes at rest, start-up, and idle CPU across a working day. Then we would list the hardest parts of the interface and prove those first, in throwaway code, before choosing anything. The structure matters more than the framework: keep state, sync and integrations independent of the UI layer, generate the API client and design tokens from their sources, and let tests enforce the boundaries. Our note on the last mile for coding agents covers how we keep the scope of builds like this in check.
06 stages
Measure the client you have, in daily use, before proposing a new one
Watchpoint
A prototype's numbers mean little without the baseline
Prove the hardest parts first: text input, streaming, large lists, theming, rich content
Watchpoint
Easy screens look finished long before the hard ones work
Let only the UI crates depend on the framework, and enforce it with a test
Watchpoint
Framework types leaking into state code make a switch a rewrite
Pin the framework to one revision and bump it behind golden renders
Watchpoint
A pre-release dependency can change drawing without a compile error
Generate the API client and design tokens; never hand-copy them
Watchpoint
Hand-kept copies drift from the web app within weeks
Treat the local store as a deletable cache, not the record
Watchpoint
A client that becomes a second source of truth will disagree with the server
The Company Twin keeps your company's records, definitions and access rules in one place, and any client, native, web or agent, reaches it through the same public gateway. Explore the Company Twin →