Back to Articles

Wails: Building Desktop Apps with Go and Web Tech Without the Electron Bloat

[ View on GitHub ]

Wails: Building Desktop Apps with Go and Web Tech Without the Electron Bloat

Hook

A Hello World Electron app weighs around 120MB. The same app in Wails? Less than 10MB. This 12x difference isn't magic—it's architectural philosophy.

Context

For the past decade, Electron has dominated desktop application development by letting developers use web technologies to build cross-platform apps. Its success spawned giants like VS Code, Slack, and Discord. But Electron's approach—bundling an entire Chromium browser and Node.js runtime with every application—comes with baggage. Each app ships with 80MB+ of browser engine, consuming hundreds of megabytes of RAM even for trivial functionality.

Wails emerged from a simple realization: every modern operating system already has a web rendering engine built in. macOS has WebKit, Windows has WebView2, and Linux distributions ship WebKitGTK. Why bundle Chromium when you can use what's already there? For Go developers who want to build desktop applications with web frontends but refuse to accept Electron's overhead, Wails offers a compelling alternative. It's not just about file size—it's about respecting system resources while maintaining the productivity of modern web development.

Technical Insight

Wails' architecture centers on a bridge between Go and JavaScript that feels almost transparent. When you expose a Go method to the frontend, Wails generates TypeScript definitions automatically, giving you type-safe calls from your UI code. Here's what that looks like in practice:

// app.go
package main

import (
    "context"
    "fmt"
)

type App struct {
    ctx context.Context
}

func NewApp() *App {
    return &App{}
}

func (a *App) startup(ctx context.Context) {
    a.ctx = ctx
}

// Greet returns a greeting for the given name
func (a *App) Greet(name string) string {
    return fmt.Sprintf("Hello %s, It's show time!", name)
}

Wails scans your Go structs and generates corresponding TypeScript definitions. Your frontend code can import and call these methods with full IDE autocomplete:

// frontend/src/App.tsx
import { Greet } from '../wailsjs/go/main/App';

function handleGreet() {
  Greet(userName)
    .then(result => setGreetMsg(result))
    .catch(err => console.error(err));
}

Notice there's no manual serialization, no REST endpoints, no gRPC definitions. The bridge handles type conversion between Go and JavaScript automatically. Pass complex structs from Go and they arrive as JavaScript objects. Return promises from the frontend and handle them as you would any async operation.

The CLI drives the development experience. Running wails dev starts a hot-reload server where your frontend rebuilds instantly on file changes, while your Go backend recompiles when needed. The build process is where Wails shows its architectural philosophy: wails build produces a single native binary. On macOS, you get a proper .app bundle with Info.plist and icon resources. On Windows, an .exe with embedded manifest and resources. On Linux, a binary that links against system WebKitGTK.

Under the hood, Wails uses platform-specific webview libraries. On macOS, it embeds WKWebView through Objective-C bridges. Windows builds rely on WebView2 (Microsoft's Chromium-based replacement for the legacy IE-based WebView). Linux uses WebKitGTK through CGo bindings. This explains both Wails' small size and its primary tradeoff: you're subject to the quirks and capabilities of each platform's native webview.

The event system provides decoupled communication between frontend and backend. Your Go code can emit events that any frontend component can listen to, useful for push notifications or progress updates:

runtime.EventsEmit(a.ctx, "download:progress", map[string]interface{}{
    "percent": progress,
    "file": filename,
})

Wails v3 (currently in alpha) introduces a new menu system, improved window controls, and plugin architecture. The v2-to-v3 migration path preserves the core bridge architecture while modernizing APIs. Templates for React, Vue, Svelte, and Angular provide starting points, but you're free to use vanilla JavaScript or any framework you prefer—Wails doesn't care what generates your HTML.

One underappreciated feature: the ability to bind Go structs bidirectionally. You can pass a struct to the frontend, have JavaScript modify it, and pass it back—Wails handles the round-trip serialization. This enables patterns like configuration UIs where Go validates business rules while React handles the form state.

Gotcha

The native webview approach creates platform-specific gotchas that Electron users never face. WebView2 on Windows requires a separate runtime installation—it's bundled with Windows 11 but needs bootstrapping on Windows 10. Your installer must handle this, or your users will see confusing errors. WebKitGTK on Linux varies by distribution: Ubuntu includes it by default, but minimal distributions might not. You'll need clear installation documentation for each platform.

Rendering inconsistencies are real. CSS that renders perfectly in Chrome might break in Safari's WebKit (macOS) or behave differently in WebView2. Features like CSS Grid, Flexbox, and modern JavaScript APIs have varying support levels across webviews. While Electron gives you a consistent Chromium environment everywhere, Wails forces you to test on each platform and potentially write platform-specific CSS workarounds. The Safari debugging experience on macOS is excellent, but debugging WebKitGTK issues on Linux can be painful without the right tools.

The Go requirement cuts both ways. If your team is already writing backend services in Go, Wails is natural. But if you're coming from JavaScript/TypeScript microservices, rewriting business logic in Go represents significant investment. The smaller ecosystem means fewer ready-made solutions—there's no Wails equivalent to Electron's massive npm package ecosystem. Need a specific native integration? You might be writing CGo bindings yourself.

Verdict

Use if: You're a Go shop building internal tools, developer utilities, or business applications where a 10MB binary beats a 120MB one. Use it when you value native OS integration, need low memory footprint for background apps, or want to leverage your team's Go expertise without learning Swift/C#/C++. It's excellent for database GUIs, system monitors, deployment tools, or any utility where "feels native" matters more than pixel-perfect cross-platform consistency. Skip if: You need guaranteed UI parity across platforms, depend on Chrome-specific APIs or extensions, require Node.js ecosystem integration (native modules, specific npm packages), or have no Go developers on staff. Skip it for consumer-facing apps where brand consistency across platforms is critical, or if your timeline can't accommodate platform-specific debugging.

// ADD TO YOUR README
[![Featured on Starlog](https://starlog.is/api/badge/developer-tools/wailsapp-wails.svg)](https://starlog.is/api/badge-click/developer-tools/wailsapp-wails)