← Back to Writing

Build Once Ship Everywhere Is Less Appealing Than Ever

Electron's promise made sense when code was expensive. Now the trade-off is backwards.

Remember when “write once run anywhere” was the dream? Cross-platform was the holy grail. Build your app in JavaScript, wrap it in Electron, ship to Mac, Windows, Linux. Done. Ship it.

Except it was never “done.” It was “done enough that your users tolerate the jank.”

What did we actually get?

Electron apps are… fine. Slack works. VS Code is great. Discord is usable. But are any of these apps actually good experiences compared to native alternatives? Slack on macOS versus Apple Messages. VS Code versus Xcode for Swift. Discord versus iMessage.

The native apps are just better. They feel right. They integrate with the system properly. They respect your battery. They don’t consume a gigabyte of RAM to show you a chat window.

The pitch was always “we’ll accept worse UX in exchange for faster development and cross-platform reach.” But was the trade-off ever actually good?

How much time does cross-platform actually save?

Here’s the thing nobody talks about. You always spend a ton of time debugging Electron bundling issues. And it’s gotten worse. The errors are obtuse. The fixes are buried in GitHub issues from 2019. Stack traces point nowhere useful.

Meanwhile, writing frontend code has never been easier. Most of it is boilerplate. Perfect for Copilot or Cursor to chew through. You can generate a whole SwiftUI view in seconds. But current models are not going to do well with why your Electron app’s auto-updater silently fails on Windows 11 with a specific antivirus configuration.

Auto-update not working on Windows. Code signing headaches on macOS. Linux packages across twelve distributions. Memory leaks from the V8 runtime that only show up in production. Native module compilation breaking on every node version bump.

I’ve built things in Electron. I’ve also built things in Swift. The Swift apps were faster to ship. Not because Swift is magical, but because I wasn’t fighting the abstraction layer. I wrote code. It compiled. It worked like a Mac app should work.

Is everyone still doing this?

Look at new apps coming out. Linear. Raycast. Arc. The exciting Mac apps of the last few years are native. They feel native. They’re fast.

Meanwhile the Electron apps that dominated 2018 are either staying put because the rewrite cost is too high, or they’re quietly starting rewrites. When’s the last time you heard someone get excited about a new Electron app?

The pattern seems clear. Legacy apps stay on Electron. New apps go native. The “build once” promise delivered, but it delivered apps that feel like compromises.

How do I build things today?

For the most part, I stay on the web. You need a good reason to ship a desktop app. It is hard to ship a desktop app. Distribution, code signing, updates, platform-specific bugs. The web gives you none of those headaches. And now you can see why Electron seemed like the way.

But when you actually need a desktop app? Swift for macOS. C# with WinUI 3 for Windows. Separate codebases.

Why? Because Swift is cheap now. Building a UI in SwiftUI is fast. The documentation is good. The tooling is good. And the result is an app that feels like it belongs on the platform.

The trade-off is… what exactly? I have to write some things twice? Most of the interesting stuff in any app is the backend anyway. The UI is the cheap part now. Especially with AI assistance making code essentially free.

Write the business logic once. Write the UI per platform. Ship native apps that users actually enjoy using.

What happens to Electron?

Electron isn’t going anywhere. The install base is too large. Too many companies have too much invested.

But I think we’ll see fewer new Electron apps. And we’ll see some high-profile rewrites. Companies that care about UX will go native. Companies that don’t care will stay on Electron and wonder why their retention numbers are weird.

The economics have shifted. Code used to be expensive enough that writing it twice seemed wasteful. Now code is so cheap that the abstraction layer’s tax seems wasteful instead.

Build once ship everywhere made sense in 2015. Does it still make sense when the “build” part is becoming cheaper and cheaper?

My bet is that we already hit the inflection point. I know I have. I have enough scars from node-gyp rebuild failed that I’m happy to get some new ones from unable to type-check this expression in reasonable time. And you know what? I can ship faster with the latter too, because AI can actually help with these errors without a 15-minute build loop.