I'm building yyzTools, a Windows productivity suite with 40+ tools fused into one install. This is the architecture decision I get asked about most.
The default path
When a developer today wants to build a desktop app, the default reflex is Electron. It's rational: HTML/CSS/JS, cross-platform, mature ecosystem (VS Code, Slack, Discord proved it works). If your team is frontend-heavy, it's almost a non-decision.
I didn't take that path. yyzTools is C++ (Win32) + WebView2 + Alpine.js + Vite. Windows-only. No bundled Chromium.
This post isn't "Electron is bad." It's "for a resident, lightweight, multi-feature desktop tool, Electron's costs outweigh its benefits, and here's what I picked instead."
Why Electron didn't fit this project
The key word is resident. yyzTools is a tool you install once and it sits in your tray, running every time you hit Alt+Space for the command palette, grab from clipboard history, fire OCR. It's not an app you open and close—it's always there.
Electron's model is "bundle a full Chromium per app." For an app you open occasionally, the ~200MB memory and multi-second startup is tolerable. For something that's always resident, it's a tax you pay every second the machine is on.
Nobody wants a clipboard manager + launcher eating 400MB of RAM just by being installed.
The alternative: WebView2 + a C++ host
Microsoft's WebView2 is a control that renders web content using the Edge engine already installed on Windows. No bundled browser. The runtime ships with Windows 11; on Windows 10 the installer guides the user.
So my stack:
- C++ / Win32 host — owns the window, system calls, process management, all native work
- WebView2 — renders the frontend pages using the system Edge
- Alpine.js (~3KB) — frontend framework, no virtual DOM, no heavy build chain
- Vite multi-entry — each feature page (command palette, OCR, translate, download...) is an independent bundle
The architecture is a four-layer sandwich:
Frontend pages (Alpine.js + vanilla JS)
↓ via web/lib/zen_api.js (unified wrapper)
WebView2 bridge layer
BindSync → synchronous return (most methods)
BindAsync → async callback (OCR, app-info)
↓
C++ NativeApi layer (FileManager / ProcessManager / ...)
↓
Win32 system API
The frontend never calls window.Zen directly. It goes through a ZenAPI wrapper class that normalizes return values—the C++ side has two JSON paths (hand-rolled strings vs boost::property_tree) where the same error field can be number 0 or string "0". The wrapper does String(res.error) === '0' to paper over it. (That's documented debt, not a hidden clever trick.)
What this buys
- Lower memory. No Chromium process resident. The main process is a thin C++ host plus the system Edge it borrows.
- Faster startup. No spinning up a fresh Chromium instance.
- Smaller disk. The web runtime is shared across the system, not bundled per-app.
- Native when needed. File ops, process management, OCR engine calls, screen recording—all C++, no IPC to a JS-land bridge for hot paths.
What it costs
Let's be honest about the tradeoffs, because this isn't a free lunch:
Windows-only. WebView2 is Microsoft's. If you want Mac/Linux, you need Electron or a full Qt port. yyzTools targets Windows, so this is fine—but it's the biggest constraint.
A hand-written bridge. Electron gives you mature IPC for free. With WebView2 you hand-roll the JS↔C++ channel (a "method name → callable" registry, sync/async variants, thread marshaling for async callbacks back to the UI thread). It's not hard, but it's code you own and maintain.
No type system on the boundary. The error field type drift I mentioned isn't caught by TypeScript—it's a runtime contract between C++ and JS. I normalize at the wrapper, but every new native method is a chance to introduce a new type mismatch. A typed IPC (like tRPC over a real protocol) would be safer; I chose simplicity.
Alpine.js means no React ecosystem. Alpine is great for "many simple pages" (which a productivity suite is). But if I wanted a complex single-page app with a rich component library, I'd miss React's ecosystem. The pages here are intentionally simple—search box, settings panel, result grid—so Alpine fits.
You need C++ people. A pure-frontend team can't extend the native side. This is a real org constraint. If your team can't staff Win32 expertise, Electron is the pragmatic call, period.
When you'd make the same call
I'd pick this again specifically when all four are true:
- The app is resident (tray, autostart, always-on) — the memory tax compounds
- Windows is the target — WebView2 is Windows-only
- You need native system access — file/process/clipboard/OCR, where C++ beats a JS bridge
- You have C++ capacity — someone can own the host and bridge
If any of those is false, Electron (or Tauri) probably wins. yyzTools happened to clear all four, so the C++/WebView2 path was correct here. It would be wrong for a cross-platform app, or a resident app with no native API needs, or a team of pure-frontend devs.
The takeaway
"Electron vs native" isn't a moral question—it's a fit question. The reflexive "always Electron" is wrong for resident, native-heavy, Windows-targeted tools. The reflexive "never Electron" is wrong for cross-platform or frontend-heavy teams.
The useful question to ask when you start a desktop project: is this app resident, and does it need native system access? If yes to both and you're on Windows, the C++ + WebView2 path is worth the C++ staffing cost. If no, Electron's defaults will serve you faster.
yyzTools is the former. Your project might be the latter. Pick on fit, not on what's trendy.
I'm building yyzTools — a free, local-first Windows productivity suite. Website: yyztools.com. I write about the architecture decisions behind it.
Top comments (0)