Why We Left Electron Behind
GitSquid v1 was built on Electron. It worked. Users could browse repositories, stage changes, manage branches, handle remotes, and use an integrated terminal -- all from a single desktop app. Under the hood, we relied on simple-git for Git operations, node-pty and xterm.js for the terminal, and the usual React stack for the UI.
But as the app matured, Electron's trade-offs became harder to ignore. We had shipped a functional product, yet the runtime itself was working against us. So we rewrote the entire backend in Rust using Tauri 2.x -- while keeping 95% of our React frontend intact.
This is the story of why we did it, how we did it, and what broke along the way.
The Problem With Electron
Electron is a proven framework. VS Code, Slack, Discord -- major products run on it. But for a Git client, the overhead is disproportionate to the task.
- App size: Electron bundles an entire Chromium browser and a Node.js runtime. Our packaged app weighed in at over 150 MB. For a Git client, that is difficult to justify.
- Memory usage: Each Electron window spawns multiple Chromium processes. Idle memory consumption sat well above what a native app would need for the same work.
- Startup time: Launching Chromium is not instant. Users noticed a perceptible delay before the app became interactive, especially on older hardware.
- Native feel: Despite Electron's efforts with native title bars and menus, the app never quite felt like it belonged on the OS. Small things -- window management, focus behavior, keyboard shortcuts -- always had a slight web-app flavor.
None of these are dealbreakers on their own. But combined, they created friction that did not align with what a developer tool should feel like: fast, lightweight, and invisible.
Why Tauri 2.x
We evaluated several alternatives before landing on Tauri. The deciding factors were straightforward:
- System webview instead of bundled Chromium. Tauri uses the OS-provided webview (WebKit on macOS, WebView2 on Windows, WebKitGTK on Linux). This alone eliminates the bulk of Electron's binary size.
- Rust backend. The backend runs as a compiled Rust binary. Memory safety, no garbage collector, predictable performance. For a tool that executes dozens of Git operations per interaction, this matters.
- Dramatically smaller binaries. A Tauri app can ship under 10 MB. Compare that to 150 MB+.
- Cross-platform with native integration. Tauri 2.x provides solid APIs for native menus, system tray, notifications, and file dialogs. The app feels like it belongs on the OS.
- Active ecosystem and good developer experience. Tauri's plugin system, IPC model, and documentation have matured significantly with the 2.x release. It was ready for production use.
The Migration Strategy
A full rewrite is risky. We mitigated that risk with a clear separation of concerns.
Keep the frontend, rewrite the backend
Our React frontend was already structured around an IPC boundary. Components called functions that talked to the Electron main process; we just needed to redirect those calls to Tauri's invoke API instead. About 95% of the React code carried over unchanged.
The backend was a different story. Every Node.js service -- Git operations, file watching, terminal management, authentication, settings -- had to be reimplemented in Rust. In total, we wrote roughly 7,200 lines of Rust and migrated over 120 IPC commands.
Git CLI over libgit2
A common question: why not use git2-rs (the Rust bindings for libgit2)?
We chose std::process::Command to call the Git CLI directly. The reasoning:
libgit2does not support every Git feature. Operations like interactive rebase, some merge strategies, and certain config options are either missing or behave differently.- Users already have Git installed. By calling their local Git binary, we guarantee behavioral parity with the command line. If it works in the terminal, it works in GitSquid.
- Debugging is simpler. The commands we run are the same commands a user would type. When something goes wrong, the error messages are familiar.
Replacing the Node.js ecosystem
Each Node.js dependency was replaced with a Rust equivalent:
simple-gitbecame direct Git CLI calls viastd::process::Commandnode-ptybecameportable-ptyfor terminal emulation- Token storage moved to the
keyringcrate, which uses the OS keychain (macOS Keychain, Windows Credential Manager, Linux Secret Service) - File watching switched to the
notifycrate - HTTP requests moved to
reqwest
Seamless settings migration
We kept the same configuration directory structure. Users upgrading from v1 to v2 do not lose their settings, saved repositories, or preferences. The Rust backend reads the same config files the Node.js backend wrote.
Challenges We Faced
The migration was not smooth sailing. Several issues cost us significant debugging time.
Null bytes in Git log parsing
We use custom format strings with git log --format to parse commit data. In the Electron version, we used specific separator characters between fields. When porting to Rust, we initially tried using null bytes (\x00) as field separators -- they seemed like a safe choice since they would never appear in commit messages.
Wrong. Rust strings handle null bytes fine, but several layers of the stack treat them as C-style string terminators. The log output was being silently truncated at the first null byte. We switched to a multi-character delimiter that would never naturally occur in Git output.
Keychain token storage
The keyring crate stores credentials in the OS keychain, which is the right approach. But we hit a similar null byte issue: we were concatenating token data with null byte separators before storing it. On retrieval, some OS keychain implementations truncated the value at the first null byte, returning incomplete credentials. The fix was the same -- use a visible, unambiguous separator.
BitBucket authentication complexity
GitHub and GitLab have relatively straightforward token-based auth flows. BitBucket is more complex. Depending on the operation and the account type, you might need a username, an email, an app password, or an OAuth token -- and the rules for which one to use where are not always obvious. Getting BitBucket auth to work reliably across clone, fetch, push, and API calls required several iterations.
Per-line staging
GitSquid supports staging individual lines from a diff, not just entire hunks. In the Electron version, patch generation happened in JavaScript. For the Tauri version, we moved this logic to the Rust backend. Generating valid Git patch format from arbitrary line selections, handling edge cases with context lines, and making sure git apply accepts the result -- this was one of the more intricate parts of the rewrite. The Rust implementation ended up being more robust than the original JavaScript version.
French apostrophes breaking builds
This one was memorable. Some of our user-facing strings contained French typographic apostrophes (the curly variety, U+2019) instead of straight ASCII apostrophes. These caused encoding issues during the build process on certain CI configurations. The fix was trivial once identified, but the error messages pointed nowhere near the actual problem.
Results
After completing the migration and reaching full feature parity, here is where we landed:
- App size: significantly reduced compared to the Electron build. The Tauri bundle is a fraction of the 150 MB+ we shipped before.
- Memory usage: lower baseline and more predictable under load. No Chromium process tree eating RAM in the background.
- Startup time: noticeably faster. The app is interactive almost immediately.
- Feature parity: 100%. Every feature from v1 exists in v2. The integrated terminal, per-line staging, multi-remote support, branch management -- all of it.
- Frontend reuse: 95% of the React codebase carried over, validating our architecture decision to keep a clean IPC boundary.
Was It Worth It?
Yes.
The rewrite took significant effort. Around 7,200 lines of Rust, 120+ IPC commands to re-implement, and several non-obvious bugs that only surfaced because Rust and Node.js handle edge cases differently. It was not a weekend project.
But the result is a fundamentally better application. GitSquid v2 starts faster, uses less memory, ships as a smaller binary, and feels more native on every platform. The Rust backend is more predictable and easier to reason about than the Node.js equivalent. And because we kept the same React frontend, users see the same interface they are already familiar with.
If you are maintaining an Electron app and the runtime overhead bothers you, Tauri is a serious option. The ecosystem is mature enough for production use, and the migration path -- especially if you already have a clean frontend/backend separation -- is more manageable than a full rewrite might suggest.
The key insight: treat your IPC boundary as an API contract. If your frontend only knows how to invoke('get_commits', { path, branch }) and does not care whether the response comes from Node.js or Rust, swapping the backend becomes a realistic project instead of a fantasy.