Por qué las aplicaciones Wails fallan en producción y cómo solucionarlo a bajo nivel
٢٦ يوليو ٢٠٢٦
0
Computing/SoftwareComments (0)
Log in to leave a comment
No posts yet
Log in to leave a comment
No posts yet
Al crear aplicaciones de escritorio con Go, Wails es una opción muy atractiva. Al no empaquetar Chromium entero como lo hace Electron, es ligero y rápido. Sin embargo, en cuanto sales de los tutoriales e intentas construir un servicio real, te chocas de frente contra un muro. La memoria de CGo empieza a tener fugas y el webview se comporta de forma distinta en Windows y en macOS.
Para un desarrollador backend sin experiencia previa con C/C++ u Objective-C, este punto se convierte en un verdadero calvario. Si no logras solucionar las fugas de memoria y la fragmentación de los webviews según el sistema operativo —problemas ocultos tras los vistosos ejemplos de la documentación oficial—, el despliegue en producción resulta imposible.
El error más común al usar CGo es asumir que el recolector de basura de Go se encargará también de la memoria en el entorno de C. Por supuesto que no lo hace. La memoria reservada mediante C.CString o C.malloc se queda en el lado de C, consumiendo memoria hasta que la aplicación muere.
También hay que tener cuidado al pasar slices de Go a funciones de C. Si pasas la dirección del propio header del slice, provocarás corrupción de memoria. Lo seguro es pasar la dirección real del primer elemento: unsafe.Pointer(&slice[0]). Al llamar a código Objective-C en macOS, tampoco debes confiar a ciegas en ARC. Los objetos creados dentro del bucle de hilos de CGo se van acumulando continuamente en el NSAutoreleasePool. Debes envolverlos explícitamente en bloques @autoreleasepool { ... } para liberarlos de inmediato.
Si estás en un entorno Windows, no hay necesidad de arrastrar el compilador de CGo (MinGW) como una pesada carga. Puedes invocar directamente las DLL mediante el paquete syscall sin el overhead de CGo. El código para cargar dwmapi.dll y activar el modo oscuro es más sencillo de lo que piensas.
`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
}
`
Al crear un módulo de control nativo, primero defines una interfaz común (system_interface.go). Luego, separas la implementación de macOS (system_darwin.go) agregando la directiva //go:build darwin junto con la lógica de Objective-C, y la implementación de Windows (system_windows.go) escribiendo Syscalls en Go puro con //go:build windows. Con solo adquirir el hábito de añadir defer C.free inmediatamente después de cada asignación en CGo, evitarás que la aplicación colapse por fugas de memoria.
Wails reutiliza el motor de webview que ya está instalado en el sistema operativo. En macOS es WebKit (Safari) y en Windows es WebView2 (Chromium). A cambio de reducir el tamaño del binario a unos 15 MB, debes gestionar tú mismo la fragmentación entre los diferentes motores de navegación.
Por ejemplo, al definir el área de arrastre en una ventana sin marco (frameless), en WebView2 basta con asignar --wails-draggable: drag. Sin embargo, en WebKit la ventana no se moverá a menos que especifiques también -webkit-app-region: drag.
El manejo de eventos también puede causar problemas. Si emites miles de eventos por segundo desde las goroutines de Go usando runtime.EventsEmit, el único hilo de interfaz de usuario del webview colapsará y la pantalla se congelará. Debes implementar un buffer en el backend que aplique throttling a los eventos con un intervalo de 60 fps (aproximadamente 16 ms). Del mismo modo, debes prevenir mediante un parche global posibles contratiempos en el frontend, como que el usuario presione F5 y borre el estado o que aparezca el menú contextual del clic derecho.
`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();
}
});
}
`
En el CSS debes incluir las propiedades de arrastre para ambos motores simultáneamente, y ejecutar applyGlobalUIFixes() en el punto de entrada de la aplicación (main.ts o App.tsx). En el emisor de eventos del backend, configura un temporizador de throttling. Con solo implementar estas medidas, solucionarás la mayoría de los comportamientos anómalos derivados de las particularidades del webview de cada SO.
Las aplicaciones Wails suelen consumir entre 35 MB y 50 MB de RAM en reposo. Es una excelente cifra comparada con los más de 200 MB que suele devorar Electron. El problema surge al transferir archivos grandes o datos binarios al frontend.
Si intercambias 50 MB de datos utilizando el enlace JSON RPC incluido por defecto, el uso de RAM se disparará momentáneamente por encima de los 180 MB durante el proceso de serialización JSON. Para evitar este fenómeno, debes implementar un streaming HTTP personalizado utilizando la opción AssetServer.AssetsHandler. Transmitir datos con un enfoque zero-copy (sin duplicación en memoria) permite mantener la memoria en reposo y en trabajo acotada entre los 22 MB y los 30 MB.
También es necesario adaptarse al entorno de los usuarios de Windows. Para aquellos clientes que no tengan instalado el runtime de WebView2, incluye la bandera -webview2 download al compilar para empaquetar el instalador guiado (bootstrapper).
`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
`
En main.go, conecta un http.Handler personalizado a AssetServer.AssetsHandler para controlar los picos de memoria al manejar grandes volúmenes de datos. Luego, añade el pipeline anterior en .github/workflows/release.yml. De esta estructura resultará que, cada vez que subas una etiqueta (tag), se generarán los binarios universales para macOS y los instaladores NSIS para Windows, publicándose automáticamente en GitHub Releases.
Si te ocupas personalmente de la gestión de memoria a bajo nivel y absorbes mediante código las diferencias entre motores de webview, podrás construir aplicaciones de escritorio sumamente robustas con Wails.