Why Wails Apps Crash in Production and Low-Level Control Methods
26 de julho de 2026
0
Computing/SoftwareComments (0)
Log in to leave a comment
No posts yet
Log in to leave a comment
No posts yet
Wails is an attractive choice when building desktop apps with Go. Unlike Electron, it doesn't package all of Chromium, making it lightweight and fast. However, as soon as you step outside tutorials to build a real-world service, you hit a brick wall. CGo memory starts leaking, and webviews behave differently between Windows and macOS.
For backend developers without prior experience in C/C++ or Objective-C integration, this boundary becomes a wailing wall. Unless you tackle the memory leaks and OS-specific webview fragmentation hidden behind the shiny examples in the official docs, deploying to production is impossible.
The most common misconception when using CGo is expecting Go's garbage collector to take care of the C memory space as well. Naturally, it doesn't. Memory allocated via C.CString or C.malloc stays in the C heap, eating up memory until the application dies.
You must also be careful when passing Go slices to C functions. Passing the address of the slice header itself leads to memory corruption. You need to pass unsafe.Pointer(&slice[0]), which is the actual address of the first element, to be safe. When calling Objective-C code on macOS, don't put blind faith in ARC. Objects created inside the CGo thread loop keep piling up in the NSAutoreleasePool. You must explicitly wrap them in @autoreleasepool { ... } blocks to drain them immediately.
In a Windows environment, there's no need to drag around a CGo compiler (MinGW) like a shadow. You can simply call DLLs directly via the syscall package without CGo overhead. Calling dwmapi.dll to enable dark mode is surprisingly straightforward.
`go
// system_windows.go
//go:build windows
package native
import (
"syscall"
"unsafe"
)
var (
modDwmApi = syscall.NewLazyDLL("dwmapi.dll")
procDwmSetWindowAttribute = modDwmApi.NewProc("DwmSetWindowAttribute")
)
const DWMWA_USE_IMMERSIVE_DARK_MODE = 20
func SetWindowsDarkMode(hwnd uintptr, enable bool) error {
var val int32
if enable {
val = 1
}
ret, _, err := procDwmSetWindowAttribute.Call(
hwnd,
uintptr(DWMWA_USE_IMMERSIVE_DARK_MODE),
uintptr(unsafe.Pointer(&val)),
uintptr(unsafe.Sizeof(val)),
)
if ret != 0 {
return err
}
return nil
}
`
When creating native control modules, start by defining a common interface (system_interface.go). Then, separate the implementations by placing Objective-C logic in the macOS file (system_darwin.go) along with the //go:build darwin directive, and writing pure Go syscalls in the Windows file (system_windows.go) with //go:build windows. Simply forming the habit of adding defer C.free immediately after CGo allocations will keep your app from crashing due to memory leaks.
Wails utilizes the webview already installed on the OS. On macOS it's WebKit (Safari), and on Windows it's WebView2 (Chromium). In exchange for shrinking binary size down to around 15MB, you have to handle browser engine fragmentation yourself.
For example, when setting up a draggable region in a frameless window without a title bar, WebView2 works fine with just --wails-draggable: drag, but WebKit will not move the window unless you explicitly declare -webkit-app-region: drag alongside it.
Accidents happen with event handling too. If you emit thousands of events per second from Go goroutines using runtime.EventsEmit, the webview's single UI thread screams and the UI freezes up completely. You need to implement a buffer on the backend to throttle events to a 60fps (~16ms) cycle. You also need to apply a global patch to prevent mishaps like users hitting F5 in the frontend and wiping application state, or bringing up the default context menu.
`typescript
// eventPatch.ts
export function applyGlobalUIFixes() {
window.addEventListener('contextmenu', (e: MouseEvent) => {
const target = e.target as HTMLElement;
if (target.tagName !== 'INPUT' && target.tagName !== 'TEXTAREA') {
e.preventDefault();
}
});
window.addEventListener('keydown', (e: KeyboardEvent) => {
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
const modifier = isMac ? e.metaKey : e.ctrlKey;
if (e.key === 'F5' || (modifier && e.key.toLowerCase() === 'r')) {
e.preventDefault();
e.stopPropagation();
}
});
}
`
In CSS, specify drag properties for both engines simultaneously, and invoke applyGlobalUIFixes() at the app entry point (main.ts or App.tsx). Add a throttling timer to the backend event emitter. With just these measures in place, most abnormal behavior caused by OS-specific webview quirks can be straightened out.
Wails apps typically consume around 35MB to 50MB of RAM. Compared to Electron, which hogs well over 200MB, it's quite lean. The issue arises when passing large files or binary data to the frontend.
Transferring 50MB worth of data through the default JSON RPC bindings causes RAM usage to instantly spike past 180MB due to the JSON serialization process. To avoid this phenomenon, you should implement custom HTTP streaming using the AssetServer.AssetsHandler option. Passing data via a zero-copy approach without memory duplication lets you lock idle and operational memory down around 22MB to 30MB.
Handling Windows user environments is also necessary. For clients without the WebView2 runtime installed, include the -webview2 download flag during build to bundle the bootstrapper together.
`yaml
name: Multiplatform Release Build
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- os: macos-latest
platform: darwin/universal
output_name: OptimizedApp-macOS-Universal
- os: windows-latest
platform: windows/amd64
output_name: OptimizedApp-Windows-Installer
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Wails
run: go install github.com/wailsapp/wails/v2/cmd/wails@latest
- name: Build macOS Universal Binary
if: runner.os == 'macOS'
run: wails build -platform darwin/universal -clean
- name: Build Windows Installer
if: runner.os == 'Windows'
run: |
choco install nsis -y
wails build -platform windows/amd64 -nsis -webview2 download -clean
`
In main.go, attach a custom http.Handler to AssetServer.AssetsHandler to fix large memory spikes. Then, drop the above pipeline into .github/workflows/release.yml. Every time a tag is pushed, a macOS universal binary and a Windows NSIS installer are generated and uploaded to GitHub Releases.
If you take care of low-level memory management directly and absorb webview engine differences through code, you can build solid desktop apps even with Wails.