DEV Community

Cover image for From 289 MB to 15 MB: What Migrating from Electron to Tauri Actually Cost
Hamdi LAADHARI
Hamdi LAADHARI

Posted on

From 289 MB to 15 MB: What Migrating from Electron to Tauri Actually Cost

WatchMe is a small macOS menu-bar app I built: it lists your running processes, you tick the ones you care about, and it notifies you the moment one of them exits. Start a long build, go do something else, get told when it's done. That's the whole product.

It was an Electron app for about two years. Last week I moved it to Tauri 2, and the number that started the whole thing was this one:

$ du -sh dist/mac-arm64/WatchMe.app
289M    dist/mac-arm64/WatchMe.app
Enter fullscreen mode Exit fullscreen mode

289 MB. To list processes and show a notification.

Why bother

I want to be precise about the motivation, because "Electron is bloated" is a lazy reason to rewrite something that works.

The app does three things that touch the OS: enumerate processes, own a tray icon, and post a notification. Everything else is a table and two checkboxes. Electron was shipping an entire Chromium build so that I could render that table — and on macOS, there is already a perfectly good webview sitting in the OS that every other app uses for free.

That's the actual Tauri pitch, and it's narrower than the marketing suggests. Tauri doesn't make your app fast. It makes your app small, by not shipping a browser. For something that lives in the menu bar and runs all day, small was the thing I wanted.

What actually changed in the code

The Electron version had a three-part IPC dance. A JSON file of channel names, a preload script bridging them, and handlers in the main process:

// ipc-channels.json
{
  "GET_PROCESSES": "get-processes",
  "GET_PREFERENCES": "get-preferences",
  "SAVE_PREFERENCES": "save-preferences",
  "UPDATE_TRAY_TOOLTIP": "update-tray-tooltip",
  "QUIT_APP": "quit-app"
}
Enter fullscreen mode Exit fullscreen mode
// preload.js
contextBridge.exposeInMainWorld('electronAPI', {
  getProcesses: () => ipcRenderer.invoke(IPC_CHANNELS.GET_PROCESSES),
  // ...four more of these
});
Enter fullscreen mode Exit fullscreen mode
// main.js
ipcMain.handle(IPC_CHANNELS.GET_PROCESSES, async () => {
  const processes = await psList();
  return processes;
});
Enter fullscreen mode Exit fullscreen mode

In Tauri, ipc-channels.json and preload.js are both gone. The channel name is the function name:

#[tauri::command]
fn get_processes() -> Result<Vec<ProcessInfo>, String> {
    let mut system = System::new_all();
    system.refresh_all();

    Ok(system
        .processes()
        .iter()
        .map(|(pid, process)| ProcessInfo {
            pid: pid.as_u32(),
            name: process.name().to_string_lossy().into_owned(),
            cmd: process.cmd().iter()
                .map(|part| part.to_string_lossy())
                .collect::<Vec<_>>()
                .join(" "),
        })
        .collect())
}
Enter fullscreen mode Exit fullscreen mode
// renderer.js
const getProcesses = () => invoke('get_processes');
Enter fullscreen mode Exit fullscreen mode

The ps-list npm package went away too — sysinfo does it in the Rust core. My frontend still reads the same three fields it always did (pid, name, cmd), so the renderer barely noticed.

The other thing that vanished was the test harness. Testing a packaged Electron app meant 579 lines of scaffolding — booting the app, driving it, tearing it down. All of it deleted. The logic worth testing was always plain JavaScript; it just hadn't been separated from Electron.

The numbers

Electron Tauri 2
.app bundle 289 MB 15 MB
.dmg 3.7 MB
npm packages in lockfile 309 73
Main process main.js, 220 lines JS lib.rs, 371 lines Rust
IPC plumbing preload.js + ipc-channels.json none
Packaged-app test harness 579 lines 0

18.9× smaller. A 94.7% reduction in what a user downloads.

Where AI actually helped — and where it didn't

The last Electron commit in the repo is dated 9 September. The migration landed on the 10th, as a single commit: 34 files, +12,154 / −4,875. Claude Code did most of the typing. Worth being specific about which parts that actually sped up, because it wasn't uniform.

What it was genuinely good at was the mechanical translation. Porting 220 lines of Electron main-process JavaScript into Rust is exactly the kind of work where a model earns its keep: the shape is known, the target API is well documented, and every mistake shows up as a compiler error in seconds. Rust's type checker is a brutal reviewer, and having one is what makes AI-written Rust tolerable — it can't quietly hand you something that half-works.

What surprised me was that the most valuable thing it did had nothing to do with the migration. While rebuilding the UI afterwards, it read the renderer and pointed out this:

row.classList.add('highlighted-row');
Enter fullscreen mode Exit fullscreen mode

That class marks a process as being watched. It has never been defined in any stylesheet. I checked every commit in the repository's history — it was added on 19 September 2024 and no CSS rule for it has ever existed. For 721 days, roughly two years, the only feedback that a process was being watched was the checkbox you'd just ticked yourself. I shipped releases with that bug and never noticed, because I wrote the code and my eyes slid straight over it.

That's the part I'd point at if someone asked what AI is actually for. Not the typing. Reading code nobody has read in two years, without any of the assumptions that made it invisible to me.

Where it didn't help is worth saying just as plainly. It verified the redesigned UI by driving it in a Chromium-based browser. But Tauri doesn't render in Chromium on macOS — it renders in WKWebView. So the verification covered a browser the app never actually runs in, and I had to close that gap by hand. An agent that can't see the real target can only tell you a thing probably works.

The catch

Here's the part the "Tauri is smaller" posts tend to skip.

The dependency count didn't go down. It went up. I removed 236 npm packages and added 499 Rust crates:

$ grep -c '^\[\[package\]\]' src-tauri/Cargo.lock
499
Enter fullscreen mode Exit fullscreen mode

309 npm packages became 73 npm packages plus 499 crates. What shrank is what I ship, not what I depend on. If your reason for migrating is supply-chain surface area, look at that number again before you start.

The build cost moved onto the developer. node_modules is 105 MB. The Rust build directory is 5.8 GB:

$ du -sh node_modules src-tauri/target
105M    node_modules
5.8G    src-tauri/target
Enter fullscreen mode Exit fullscreen mode

Clean release builds take minutes rather than seconds. I traded a big download for my users against a big build for me — which is the right trade for an app with more users than developers, but it is a trade.

You don't ship a browser, so you don't control the browser. Tauri uses WKWebView on macOS, WebView2 on Windows and WebKitGTK on Linux. On macOS the webview is a core OS component, which means, per Tauri's own docs, unsupported macOS versions don't get WebKit updates. My minimumSystemVersion is 13.0, so my CSS floor is whatever Safari shipped with macOS 13 — not whatever's current. Every appearance: none and :focus-visible in my stylesheet is a bet on that floor. On Electron I never had to think about it, because I was shipping the renderer.

Wrapping up

WatchMe is on Tauri 2.11.5, wry 0.55.1 and sysinfo 0.39.6, built with rustc 1.98.1 and Node 22. The app is 15 MB instead of 289 MB, and the code that does the interesting work is smaller and better tested than it was.

Would I do it again? For a menu-bar utility, yes, without hesitating. For anything with a complex canvas-based UI, I'd think much harder — three webview engines means three sets of rendering quirks, and the more unusual your rendering is, the more of them you'll find. That's the same trade as above, with a lot more at stake.

The repo is at github.com/killerwolf/watchme if you want to see what 371 lines of Rust and a table of checkboxes look like.

References

Top comments (1)

Collapse
 
raknaos profile image
Baptiste Le Bouquin

Honest numbers beat the usual "Electron is bloated" take — 289 MB for a process lister is a good poster child. The part I'd love to see quantified next is the migration cost in wall-clock time: how much of the two-year Electron history did you have to rework (tray APIs, auto-update, notifications), and did Tauri's notification behavior on recent macOS match what you had?

We keep a small Electron tool on the roadmap for the same rewrite. The one thing that would decide it for us is whether the Tauri updater survives macOS signature changes across versions — that bit me harder than anything size-related.