Repository navigation
RFC: Move the desktop shell from Tauri to Electron #174
Description
Status
Proposed. No migration work is scheduled. This RFC collects the evidence and the plan so the decision can be made once.
Summary
Move the desktop shell from Tauri 2 (system webviews: WKWebView on macOS, WebView2 on Windows, WebKitGTK on Linux) to Electron (bundled Chromium). The React frontend, the Bun sidecar and the CLI stay as they are; only the shell around them changes.
Why
1. WebKit layout is the remaining cause of streaming jank
After #169 and #171, React work per streaming flush is 1–3 ms. What's left is the engine's layout. Measured in #172 (same frontend, same Vite server, 60 KB reply, 750 flushes, 3 runs each):
| Engine | Layout per flush, median / p95 |
|---|---|
| Chrome 156 | ~1 ms / ~2.4 ms |
| WKWebView | ~19–20 ms / ~265 ms |
About 20× at the median and 100× at p95. WKWebView spends 250–350 ms on a flush that starts a heading or block KaTeX, and 25–45 ms on new table rows, code lines and paragraphs; Chrome stays around 1–3 ms for all of them. Opening a session with 200–1000 messages took 0.7–3.2 s to mount in WKWebView and 40–80 ms in Chrome. No frontend change gets WebKit within 20× of this.
Caveat: Chrome is not Electron's bundled Chromium, and this was a dev build. The engine is the same, so the gap should hold; the first migration step re-measures inside Electron.
2. One engine on every platform
Today each platform renders with a different engine, so a fix verified on macOS can behave differently on Windows, and Linux depends on WebKitGTK (#25 is a 12 GB+ memory freeze on Linux that has had no progress since June). With Electron, all three run the same Chromium.
3. A built-in browser the agent can drive
A built-in browser is planned. If the agent is to drive it, it needs a uniform automation protocol (Chrome DevTools Protocol). macOS's system webview doesn't offer one; Electron does, through WebContentsView and its debugger API.
4. Tooling
Electron has mature auto-update (electron-updater), signing and notarization tooling (electron-builder), and Chromium DevTools in every build.
Cost
- Installer size. v0.6.1 is 42 MB (macOS
.dmg) and 43 MB (Windows-setup.exe), of which the Bun sidecar is ~71 MB uncompressed. Electron adds roughly 80–100 MB uncompressed (about 40–60 MB compressed) per platform. - Memory. Chromium's renderer and GPU processes add roughly 100–200 MB at idle compared with a system webview. Each session already runs a CLI process of ~150–220 MB, so the relative increase shrinks as sessions are opened.
- Migration work (from
desktop/src-tauriatmain@5e14099): about 2,600 lines of Rust and 20 commands, plus six plugins (shell, dialog, process, updater, notification, single-instance) and the tray. The frontend touches Tauri in 19 files:api/core(invoke) 23 imports,plugin-dialog11,api/window8,plugin-notification5,api/event5,plugin-process4,api/webview4,plugin-updater3,plugin-shell3,api/app1. - Release pipeline.
release.yml,smoke-test.yml, the macOS/Windows build scripts andscripts/release.tsare rewritten forelectron-builder.
Plan
- Thin the shell first (Keep config and terminal logic in the sidecar instead of duplicating it in the Rust shell #173). Move config-directory resolution and terminal settings into the sidecar, so the shell keeps only OS-level work. This is worth doing whether or not we migrate, and it shrinks what has to be ported.
- Put one interface between the frontend and the shell. Replace direct
@tauri-apps/*imports with a smalldesktop/src/lib/shellmodule (invoke, window, dialog, notification, updater, events). The Tauri build keeps working through it; the Electron build implements the same module overcontextBridge/IPC. - Electron spike. Main process that starts the sidecar, opens the window and implements the shell module; re-run the Measure streaming layout cost in long sessions #172 bench inside Electron and record installer size and idle memory against the Tauri build. Go/no-go here.
- Port the rest. PTY terminal (
node-pty, or move it into the sidecar as Keep config and terminal logic in the sidecar instead of duplicating it in the Rust shell #173 suggests so H5 can use it too), tray, single instance, window state, notifications, app mode, zoom, session export. - Release pipeline.
electron-builderfor.dmgand NSIS, signing and notarization,electron-updaterreplacing Ship in-app auto-update with electron-updater #118, smoke tests. - Ship behind a release candidate, then remove
desktop/src-tauri.
What this changes for other work
- Ship in-app auto-update with electron-updater #118 (auto-update with our own signing key) and Upgrade Tauri from 2.10.3 to 2.12 #141 (Tauri 2.12) are on hold: both invest in Tauri.
- The component library move (shadcn/ui) is independent of the shell. Do it before or after this, not at the same time.
- [bug:critical] Linux: app freezes with 12GB+ RAM usage during normal use #25 (Linux memory) is likely resolved by not using WebKitGTK; re-test after the spike.
Open questions
- Electron's Chromium vs the Chrome numbers: confirmed in step 3.
- Do we keep the Bun sidecar as a separate process, or run parts of it in Electron's main process? Keeping it separate is the smaller change and keeps H5 working unchanged.
- Minimum macOS and Windows versions after the move.
- Scrolling up through a long history was not measured in Measure streaming layout cost in long sessions #172; measure it in the spike for both engines.
Activity
refactor(desktop): thin the shell and route all shell calls through o…
GoDiao commentedon Oct 25, 2026
Progress: steps 1 and 2 are done in #179 (
26f91b7).desktop/src/lib/shellis now the only module indesktop/srcthat imports@tauri-apps/*. The 19 files that used Tauri directly call named functions there instead, and a test fails if anything else imports@tauri-apps/. An Electron build would implement this one module.Next is step 3: an Electron spike that starts the sidecar, opens the window and implements
lib/shell. Then re-run the #172 layout bench inside it, and record installer size and idle memory against the Tauri build. That is the go/no-go point. Not scheduled yet.GoDiao commentedon Oct 25, 2026
Step 3 results: Electron spike
Recommendation: go. Electron's bundled Chromium matches the Chrome numbers from #172, long sessions open and scroll about 10× faster than in WKWebView, and memory is not higher by the Activity Monitor metric. The cost is installer size: 152.7 MB vs 46.2 MB.
Measured on an M1 Pro running macOS 26.6.2 (the same machine and OS as #172), on
main@26f91b7plus the spike. Electron 45.0.0 (Chrome 156.0.8078.48) vs WKWebView (system WebKit).What was built
desktop/electron/: a main process that starts the Bun sidecar with the same arguments and environment as the Tauri shell (reserved port,--app-root,DREAMCODER_PARENT_PID, H5 dist dir,XDG_CACHE_HOME) and kills it on quit. It opens the window either on the Vite dev server or ondist/, which in production is served fromdreamcoder://localhost. A preload script exposesinvoke/listenthroughcontextBridge(contextIsolationandsandboxon).desktop/src/lib/shell/electron.ts: an Electron implementation of everylib/shellexport. A test type-checks it againsttypeof import('./index'). A Vite resolver plugin swaps it in only whenDREAMCODER_SHELL=electron, so the Tauri build is unchanged and its bundle has no Electron code. The boundary test still holds.electron-builderproduces an unsigned macOS arm64.dmg. The sidecar andrggo toContents/Resources/bin, and the packaged app starts and serves the UI.lib/shellcoverage:checkForAppUpdatereturnsnull;onFileDropis a no-op, so drops go through the composer's DOM handlers by content rather than by path.setAppMode,prepareAppModeRestart,writeSessionExport,prepareUpdateInstall,cancelUpdateInstall,startDragging.Streaming layout per flush
Same bench, method and inputs as #172: 60,000-char reply, 80 characters every 50 ms, 750 flushes, 3 runs pooled, dev build with StrictMode, 1280 px content. The WKWebView and Chrome rows are the #172 results. They were not re-run because the machine and OS are the same, and since
5e14099the only change undercomponents/chatandchatStoreis #179's import path.Electron's per-run layout medians were 0.9–1.3 ms, and DOM size matched #172 (13.4k / 14.6k / 15.0–15.4k elements). The "Chrome is not Electron's Chromium" caveat in the RFC is answered.
Mounting a session: 200 messages took 64–102 ms and 1000 messages 49–138 ms in Electron, vs 0.7–3.2 s in WKWebView (#172).
Scrolling up through history (new)
Method: seed 1000 finished messages, scroll the transcript container to the bottom, then decrease
scrollTopby 120 px once perrequestAnimationFrameuntil it reaches the top. Measured: wall time and the interval between frames. The bench ran in scroll-only mode, 3 page loads per engine, window in the foreground, on a 120 Hz display.The frame counts differ because the virtualizer replaces estimated heights with measured ones while scrolling. WKWebView ran at DPR 1.6 and Electron at DPR 2, which if anything favours WKWebView.
Size
macOS arm64, unsigned. Both
.dmgfiles are UDZO..dmg.appdist19 MiBElectron adds 107 MB to the installer and 250 MiB to the installed app, about twice the RFC's estimate (40–60 MB compressed, 80–100 MB uncompressed). Keeping only the en and zh_CN Chromium locales is already included.
Memory
Method: packaged release builds of both shells. Each run started from a fresh config with only a mock provider. The metric is
phys_footprintfrom/usr/bin/footprint(what Activity Monitor shows as Memory), summed over the app's processes. For Tauri this includes the WebKit GPU, Networking and WebContent processes. "Idle" was taken 30 s after startup with the main UI open and no session. "One session" was taken 30 s after a session was opened in the window and one mocked reply streamed into it. Values are the median of 3 runs.Per process, Tauri: main 28 MiB, WebContent 98–112, GPU 31–33, Networking 8.5. Electron: main 43 MiB, renderer 43–50, GPU helper 44, network helper 7. Sidecar 67–69 MiB plus 89 MiB for the CLI with a session, the same in both. Summing RSS instead puts Electron at about 2× Tauri (277–360 vs 129–185 MiB), because RSS counts the shared Chromium framework pages once per process. The RFC's "+100–200 MB at idle" does not hold by footprint.
What step 4 still needs
node-ptyin the main process (a native module rebuilt for every Electron upgrade), or move it into the sidecar as Keep config and terminal logic in the sidecar instead of duplicating it in the Rust shell #173 item 3 suggests.electron-updater).startDragging. The title bar needs-webkit-app-region: draginstead ofuseWindowDrag.webUtils.getPathForFilein the preload.process.env, which is minimal when the app is launched from Finder.dreamcoder://localhostto the server's local origins explicitly (it is accepted today only because its hostname islocalhost).isTauriRuntimeto something shell-neutral.Dependencies: the frontend and the sidecar keep theirs.
@tauri-apps/*andsrc-tauri(with its Rust dependencies) go away. The new runtime dependency iselectron-updater, plusnode-ptyif the terminal stays in the shell, so the terminal's location should be decided first. Building no longer needs Rust, butbuild-sidecars.tsusesrustcto detect the target triple and would need to change. Packaging needs Node.Not verified: signed builds, Windows and Linux, a real model, memory over a long real session, multi-client and reconnect paths under Electron.
Checks
bun test: 1393 pass.desktop:lintis clean andtesthas 1284 passed, 4 skipped.tscfordesktop/electronis clean.The spike is on a local branch with three commits and has not been pushed yet.
Scroll pass added to the #172 bench (temporary, not for merging)
Added to
desktop/src/bench/layoutBench.tsxfrom the #172 patch.runAllcallsscrollPass(historyMessages(1000))after the streaming passes and posts the result to/scroll.VITE_LAYOUT_BENCH_SCROLL_ONLY=1skips the streaming passes. The page reloads itself until 3 runs are done (counted insessionStorage).GoDiao commentedon Oct 25, 2026
Decision: go. The spike results are in the comment above. The port (step 4) continues on the spike branch.
Decisions for the port:
node-ptyto the Electron main process. One place owns it, a later shell change doesn't touch it, and the H5 page can use it too.electron,electron-builderandelectron-updater. Nonode-pty.#118 (Tauri auto-update) and #141 (Tauri 2.12) stay on hold and will be closed when the port lands.
GoDiao commentedon Oct 25, 2026
Update to the dependency list above: no
electron-updaterfor now. There is no update feed yet, and on macOS it needs a Developer ID signature we don't have, so the port keeps today's behaviour (no auto-update) and the updater comes with #118. New dependencies areelectronandelectron-builderonly.