Same Console, Two Shells, Real Numbers.
The Atlas Ops Console is the fleet-dispatch client we built for an 8,000-endpoint logistics operator. It ran on Electron for two years. In June we rebuilt the shell on Tauri with a Rust core, kept the renderer bundle untouched, and measured both on the same three machines. Numbers first, then reasons, then what Tauri cost us.
The Setup — Same Hardware, Same UI Code, Two Shells
We did not want a synthetic benchmark. We wanted to know whether swapping the shell changed anything an operator could feel, and anything procurement would ask about. So the constraint from day one: one renderer bundle, two hosts.
The UI is React — a virtualized endpoint grid, a command palette, a live dispatch feed. Both shells mount that exact bundle. The only divergence is the bridge, ~400 lines per side translating invoke() calls into IPC. Everything below it is Chromium/Node or WebView2/Rust. That is the whole experiment.
Hardware was deliberately unflattering:
- M2 MacBook Air, 16 GB — the developer machine.
- Four-year-old i5 ThinkPad, 8 GB, Windows 11 — the office machine.
- Panasonic rugged terminal, Celeron N4500, 8 GB — the fleet hardware, and the reason this was worth doing.
Method: clean boot, settle, launch, then sit idle five minutes before sampling. Memory and CPU came from Instruments and Windows Performance Recorder, sampled per process tree so no child escaped. Cold start is spawn to first painted grid; warm start is the second launch of that boot. IPC latency is 200 round-trips of 120 KB, first 20 discarded. Every figure is the median of 10 runs per machine.
The Numbers
Same UI, same data, same machines, same day of builds:
| Metric | Electron 30.1 | Tauri 2.6 | Delta |
|---|---|---|---|
| Idle memory (5 min) | 400 MB | 42 MB | −89% |
| Cold start | 2,480 ms | 740 ms | −70% |
| Warm start | 1,120 ms | 310 ms | −72% |
| Installer size | 92.4 MB | 8.7 MB | −91% |
| Installed size | 310 MB | 26 MB | −92% |
| Idle CPU | 1.8% | 0.3% | −83% |
| IPC round-trip (120 KB) | 61 ms | 18 ms | −70% |
| Release build time | 3 min 40 s | 1 min 12 s | −68% |
The harness is boring on purpose — boring is what reproduces. It runs on every release branch:
# idle footprint — median of 10 clean-boot runs, per shell
pnpm bench:desktop --shell=tauri --idle=300 --runs=10
pnpm bench:desktop --shell=electron --idle=300 --runs=10
# IPC round-trip: 200 warm calls, drop the first 20, emit p50
bench ipc --payload=120KB --runs=200 --trim=20 --emit=p50
# per-process RSS across the whole tree (Windows)
powershell -c "(Get-Process Atlas,msedgewebview2 |
Measure-Object WorkingSet64 -Sum).Sum / 1MB"
# CI guard — fail the build if the idle budget creeps
if [ "$RSS_MB" -gt 64 ]; then echo "budget blown" && exit 1; fi
# the bridge: page-sized batches, never per-row
let rows: Vec<EndpointRow> = serde_json::from_str(&payload)?;Two caveats. Tauri’s installed size excludes WebView2 — already present on every supported Windows 10/11 machine, with ~1.5 MB of bootstrap on a bare image. And the 42 MB includes our Rust cache of the last 500 endpoint rows: a hollow app reads lower, a cache-starved one higher.
Why the Gap Exists
Electron’s 400 MB is not bad engineering — it is a browser you did not open. At idle: five processes (main, GPU, two utility, one renderer), each with its own heap and V8 isolate. Chromium amortises that across a dozen tabs; shrink it to one window and you still pay full freight.
Tauri pays a different bill: WebView2 on Windows, WKWebView on macOS, WebKitGTK on Linux — already resident because Edge or Safari or the distro put them there, and patched by the OS on a cadence we do not have to staff. Our process carries the shell, the bridge, and the core. Nothing else.
The 400 MB versus 42 MB comparison is the most quoted number in this space and the least decisive. It is mostly a packaging decision, not a code-quality judgement.
Which cuts both ways. We hold 42 MB because the Rust core pages the endpoint table instead of pinning all 8,000 rows — an Electron team can make that call too. Give Tauri four windows and a resident WASM index and it will eat 300 MB. The shell sets the floor; your data model sets the ceiling.
Not memory — start time. Dispatchers alt-tab dozens of times a shift: 2,480 ms to 740 ms cold, 1,120 ms to 310 ms warm. That is what operators commented on unprompted. Memory mattered to IT; start time mattered to the humans.
The IPC Question
Eighteen milliseconds is not magic; it stacks: serialize, hop the channel, deserialize in Rust, run the handler, mirror it back. For our 120 KB page the split was about 5.4 ms of serialization, 0.9 ms of channel, 11.7 ms of handler — filter and sort over the cached snapshot. Electron’s 61 ms: 22 ms of structured clone (object graphs are expensive, flat records are not), 4 ms of channel, 35 ms of handler time competing with the renderer on the same loop.
Our first implementation was far worse: one invoke per visible row — hundreds of round-trips, seconds of chatty traffic. The fix was structural: batch to page-sized payloads. Ship 120 KB once, slice it locally in the webview. Ten lines of bridge code turned an unusable grid into one where filtering 8,000 endpoints feels immediate.
When does 18 ms matter?
- Matters: keystroke-to-row filtering, the command palette, dispatch state an operator is waiting on, telemetry flushes where dropped data matters.
- Does not matter: opening a modal, a report you read for a minute, background sync, log tailing.
- Never: per-frame IPC. Run motion in the webview on
requestAnimationFrame; push aggregates to the core at 10 Hz. We throttle the dispatch feed to 8 Hz and nothing downstream can tell.
Sixty milliseconds would have been fine for four of those five. We optimised for the fifth — the one dispatchers touch all day. Our performance work runs the same way: optimise the most repeated interaction, not the highest-profiled one.
What You Give Up With Tauri
The migration was not free. Four costs were real.
WebView variance. Electron pins one Chromium version; Tauri renders on whatever the OS ships. WebView2 tracks Edge’s evergreen channel, so behaviour can shift under you mid-week — a Windows update changed grid text weight visibly. WKWebView moves on Apple’s schedule and deprecates APIs in point releases. WebKitGTK varies by distro badly enough that we dropped the Linux target and said so in the contract. Font rendering differs between Core Text and Skia. None of it is fatal — all of it needs a smoke pass on real machines, now nightly.
Ecosystem depth. Electron has a decade of native modules, crash reporters, auto-update providers, and answers to questions you have not asked yet. Tauri covers the common 80%; the rest is Rust you write. We needed Zebra label printing over serial and ended up with a ~600-line plugin — tested, working, and ours to maintain forever.
Signing and the updater. The requirements did not change — Apple still wants notarization, Windows still wants Authenticode — but the toolchain did: a Mac in CI, Rust provisioned beside Node. The sharp edge is the updater: an Ed25519 key and a static manifest endpoint. Rotate that key carelessly and you have no rollback path. Ours runs off S3, and the first thing we tested was the downgrade.
Debugging. Attaching DevTools to the webview is fiddly — on Windows it needs WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port, and releases ship without it. On the Rust side you chase panics that only reproduce under --release. Budget one person fluent in both runtimes; ours was, and the second still needed two weeks.
The Security Model
The enterprise case for Tauri is not “it is safer” as a slogan — it is that the surface is small enough to describe in a document a procurement reviewer can read.
Tauri’s capability model has three levels. Command allowlists decide which APIs the webview can reach at all. Scoped permissions constrain what an allowed command may touch — filesystem access pinned to $HOME/app-data globs instead of fs:all, no blanket shell execution, no open-ended fetch. And the CSP is injected and enforced by default: no remote script origins, no inline eval. The Electron equivalent means hand-stripping nodeIntegration, disabling the remote module, and maintaining a CSP someone can silently revert. Here it is off by default; you opt in with intent.
Smaller compounds: about 1,100 npm packages became roughly 180 web-side plus 90 Rust crates. The SBOM shrank by an order of magnitude, the CVE scan surface with it — and the question every security team asks, who patches the embedded browser?, now has an answer we can prove with an inventory report, put in writing in our engagement reviews.
One caveat: an allowlist is only as good as the commands you expose. Every #[tauri::command] taking a path or a query is an RPC endpoint facing your own renderer — validate inputs, scope outputs, and never treat the webview as trusted just because you wrote it.
When Electron Is Still the Right Answer
We moved our own product and would still recommend Electron in four situations.
- Pixel-identical rendering is a requirement. Design tools, video editors, CAD viewers — anything where “it looks the same everywhere” is contractual. One Chromium build deletes an entire QA matrix.
- Heavy offline media. WebCodecs, large WASM heaps, MSE playback, sustained canvas or WebGL. Chromium’s GPU process is battle-tested on a Celeron in a way WebKitGTK is not.
- The team is already fluent. Fluency beats runtime performance in delivery terms. A team shipping Electron in days will ship Tauri in weeks while learning Rust on the client’s clock.
- Memory is not the constraint. A workstation with 32 GB, a session open twenty minutes at a stretch, one user per machine: 400 MB is not a line item. The binding constraint is the schedule.
For most desktop software, 400 MB is not why the product fails — it is why it is annoying. Different problems, different budgets.
The Decision Matrix
The version we hand clients who do not want the essay:
| Situation | Recommendation | One-line reasoning |
|---|---|---|
| Latency-critical tooling | Tauri, batched IPC | 18 ms p50 at page granularity. |
| Memory-constrained fleet hardware | Tauri | 42 MB on an 8 GB Celeron, not 400 MB. |
| Marketing-heavy UI | Electron | Pixel parity and one pinned Chromium beat a smaller binary. |
| Long-lived internal tool | Tauri | Smaller SBOM, OS-patched webview, five-year life. |
| Regulated environment | Tauri, if the team can carry Rust | Capability allowlists plus an answerable patching question. |
Verdict. We moved Atlas to Tauri and kept every renderer line: 42 MB instead of 400, 740 ms instead of 2,480, 18 ms instead of 61, an 8.7 MB installer instead of 92. Real wins on hardware our client owns. But the deciding argument was never the memory column — it was start time, patch ownership, and a security review that stopped being a conversation. We would do it again on a fleet console, not on a design tool. Making the call live? Bring us the constraints.
Pick the Shell With Evidence, Not Vibes.
If you have a desktop client to build, rescue, or migrate, thirty minutes with a principal engineer is usually enough to tell whether it is a shell decision, an architecture decision, or neither.