DEV Community

Cover image for 5 desktop app pain points that pushed me to build my own Go framework
Naman vyas
Naman vyas

Posted on

5 desktop app pain points that pushed me to build my own Go framework

I wanted to ship a desktop version of a CLI tool I had written in Go. Three mainstream options in 2026: Electron, Tauri, or native bindings via Fyne, Gio, or Qt. I tried the first two. Here are the five things that pushed me toward writing my own framework instead.

None of this is a hit piece on Electron or Tauri. Both are mature, both have real advantages. But the issues below kept stacking, and eventually writing something new felt cheaper than fighting the tradeoffs.

1. Your "hello world" is 180 MB (Electron)

A fresh Electron app with a blank window weighs in around 180 MB zipped and close to 400 MB installed. That is before you write a line of UI code. Idle memory sits between 150 and 300 MB RSS depending on platform.

For a lot of apps this is fine. For anything you want users to download as a simple tool, it is a non-starter. Part of your user acquisition funnel is now "willing to download 200 MB for a thing they have not tried yet."

Related: every Electron app ships its own copy of Chromium. Ten Electron apps on your machine means ten copies of the browser.

2. Tauri fixed bundle size, but Linux became a lottery

Tauri drops the binary to under 10 MB by using the OS's native webview. Great on macOS (WKWebView) and Windows (WebView2). Linux is where it gets complicated: WebKitGTK.

Ubuntu 24.04 and later moved libwebkit2gtk-4.0-dev out of the default packages. Tauri users on newer distros have to install libwebkit2gtk-4.1-dev manually and pass a -tags webkit2_41 flag. Fedora has deprecated the 4.0 package. There is an active discussion on the Tauri repo asking to replace WebKitGTK with Chromium entirely because of instability (tauri-apps/tauri#8524).

On the plus side: much smaller binaries, better security defaults out of the box. On the minus side: your Linux users' first impression is a dependency error.

3. The web ecosystem stops at the OS boundary

The pitch is compelling: "use React, get the entire npm ecosystem." That is true for the UI layer. It stops being true the moment you want:

  • Native menus
  • A tray icon
  • A file picker that looks like the rest of the OS
  • OS notifications
  • File associations
  • A dock badge (macOS) or taskbar overlay (Windows)

Each of these needs framework-specific bindings. Electron has Menu, Tray, dialog, nativeTheme. Tauri has plugins and IPC commands. Either way, you end up with a React UI and a pile of JSON crossing a bridge to Rust or Node, which then calls into native code.

That bridge is a real cost. It has to be typed by hand, serialized on every call, and debugged whenever the contract drifts. The "smaller ecosystem" argument critics aim at native frameworks does not disappear. It just moves one layer down.

4. You are now maintaining two toolchains

Electron: you need Node, a package manager, a bundler (webpack, vite, esbuild), a TypeScript setup, plus whatever your backend is in. If your team does not already run a web stack, you do now.

Tauri: you need Rust, Cargo, AND Node for the frontend. Two languages, two package managers, two upgrade paths. Fine if you already enjoy Rust. Less fine if Rust was never the goal.

For a team whose primary language is something else (Go, Python, Java), this is the hidden cost of "just ship a desktop app." You are also becoming competent in a second stack.

5. Shipping is harder than it looks

This one is not framework-specific, but both frameworks inherit it:

  • Code signing on macOS and Windows
  • Notarization (macOS)
  • Auto-updating (each OS has a different story)
  • Linux packaging: AppImage vs Flatpak vs .deb vs Snap
  • Icon formats (.icns, .ico, .png, per platform)

Electron has electron-builder (mature, full of quirks). Tauri has its own bundler (improving fast). Both work. Both take a weekend the first time. The framework cannot abstract this away because it is an OS problem, not a framework one.

Worth knowing before you start: the ship-to-users step is usually as much work as the app itself.

Where I ended up

I was already writing the backend in Go. What I wanted was "desktop UI that is as simple to ship as a Go binary." No embedded browser, no Rust boundary, no Node build step. Declarative components because the alternative (imperative widget trees) gets unmaintainable fast.

So I built Gova: a declarative GUI framework for Go built on Fyne. Typed struct components, reactive state, real NSAlert, NSOpenPanel, NSDockTile on macOS via cgo, Fyne fallbacks on Windows and Linux. Single static binary, 32 MB for a counter app. Pre-1.0, MIT.

package main

import g "github.com/nv404/gova"

type Counter struct{}

func (Counter) Body(s *g.Scope) g.View {
    count := g.State(s, 0)
    return g.VStack(
        g.Text(count.Format("Count: %d")).Font(g.Title),
        g.HStack(
            g.Button("-", func() { count.Set(count.Get() - 1) }),
            g.Button("+", func() { count.Set(count.Get() + 1) }),
        ).Spacing(g.SpaceMD),
    ).Padding(g.SpaceLG)
}

func main() {
    g.Run("Counter", g.Define(func(s *g.Scope) g.View {
        return Counter{}
    }))
}
Enter fullscreen mode Exit fullscreen mode

go run . and a real native window opens. That is the whole pipeline.

Repo: github.com/NV404/gova
Docs: gova.dev

Gova is not trying to replace Electron or Tauri for the cases they are good at. Both are still the right call for a lot of apps. Gova is for Go-first teams who do not want to take on a web toolchain just to ship a desktop app.

If any of the five pain points above felt familiar, take a look. A star on the repo is genuinely the single biggest thing you can do for a new library. It is the signal other devs use when they are deciding whether something is worth trying, and it is how small projects reach the people who would find them useful.

Top comments (0)