Pourquoi les applications Wails plantent en production et comment les contrôler à bas niveau
2026年7月26日
0
Computing/SoftwareComments (0)
Log in to leave a comment
No posts yet
Log in to leave a comment
No posts yet
Lorsqu'il s'agit de développer des applications de bureau en Go, Wails représente un choix très séduisant. Contrairement à Electron, il ne packages pas l'intégralité de Chromium, ce qui le rend léger et rapide. Cependant, dès que l'on dépasse le stade du simple tutoriel pour construire un véritable service, on se heurte immédiatement à un mur : la mémoire CGo commence à fuiter et le webview se comporte différemment sous Windows et macOS.
Pour un développeur backend sans expérience préalable de l'interopérabilité C/C++ ou Objective-C, cette étape s'apparente au mur des lamentations. Si vous ne maîtrisez pas les fuites de mémoire et la fragmentation des webviews selon l'OS — qui se cachent derrière les exemples étincelants de la documentation officielle —, tout déploiement en production devient impossible.
L'erreur la plus courante lors de l'utilisation de CGo consiste à penser que le garbage collector de Go va également s'occuper du monde C. Évidemment, il n'en fait rien. La mémoire allouée via C.CString ou C.malloc reste dans l'espace C et dévore de la mémoire jusqu'au crépuscule de l'application.
Un soin particulier doit aussi être apporté lors du passage de slices Go à des fonctions C. Transmettre l'adresse du header de la slice provoque une corruption mémoire. Pour rester en sécurité, il faut passer l'adresse réelle du premier élément, à savoir unsafe.Pointer(&slice[0]). Lorsque vous appelez du code Objective-C sous macOS, vous ne devez pas vous fier aveuglément à ARC. Les objets créés à l'intérieur de la boucle de thread CGo s'accumulent continuellement dans le NSAutoreleasePool. Il est indispensable d'envelopper explicitement ces blocs avec @autoreleasepool { ... } pour purger la mémoire immédiatement.
Sous Windows, il n'est nullement nécessaire de traîner le compilateur CGo (MinGW) comme un boulet. Vous pouvez tout à fait vous passer du surcoût de CGo en attaquant directement les DLL via le package syscall. Le code permettant de charger dwmapi.dll pour activer le mode sombre se révèle plus simple qu'il n'y paraît.
`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
}
`
Pour concevoir un module de contrôle natif, commencez par définir une interface commune (system_interface.go). Ensuite, placez la logique Objective-C dans l'implémentation macOS (system_darwin.go) avec la directive //go:build darwin, et isolez les appels Syscall en Pure-Go dans l'implémentation Windows avec //go:build windows. Le simple fait de prendre l'habitude d'ajouter defer C.free immédiatement après une allocation CGo vous évitera à coup sûr les crashs d'application dus aux fuites de mémoire.
Wails réutilise le webview déjà installé sur le système d'exploitation : WebKit (Safari) sur macOS et WebView2 (Chromium) sous Windows. En contrepartie d'une taille de binaire réduite à environ 15 Mo, vous devez gérer directement la fragmentation entre les moteurs de rendu.
Par exemple, pour configurer une zone de glissement sur une fenêtre sans bordure (frameless), WebView2 fonctionne avec la simple propriété --wails-draggable: drag, tandis que WebKit n'autorise le déplacement de la fenêtre que si vous spécifiez également -webkit-app-region: drag.
Des problèmes surviennent aussi au niveau de la gestion des événements. Si vous émettez des milliers d'événements par seconde depuis des goroutines Go avec runtime.EventsEmit, le thread UI unique du webview va être saturé, provoquant le gel complet de l'écran. Il convient donc de placer dans le backend un tampon pour réguler les événements sur un cycle à 60 FPS (environ 16 ms). De plus, il faut bloquer par un correctif global les incidents de parcours côté frontend, comme un utilisateur qui rafraîchit l'état avec F5 ou l'apparition involontaire du menu contextuel par clic droit.
`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();
}
});
}
`
Dans le CSS, vous devez renseigner simultanément les propriétés de glissement propres aux deux moteurs, puis exécuter applyGlobalUIFixes() au point d'entrée de l'application (main.ts ou App.tsx). Côté backend, appliquez un minuteur de régulation (throttling) sur l'émission des événements. Ces quelques précautions suffisent à éradiquer la majeure partie des comportements anormaux causés par les spécificités des webviews de chaque OS.
Une application Wails consomme habituellement entre 35 Mo et 50 Mo de RAM. C'est extrêmement raisonnable par rapport à Electron, qui en dévore facilement plus de 200 Mo. Les complications apparaissent lors du transfert de gros fichiers ou de données binaires vers le frontend.
L'échange d'un bloc de données de 50 Mo via les liaisons JSON RPC intégrées par défaut peut faire grimper la consommation de RAM en flèche au-delà de 180 Mo durant le processus de sérialisation JSON. Pour éviter ce phénomène, il faut implémenter un système de streaming HTTP personnalisé à l'aide de l'option AssetServer.AssetsHandler. En transférant les données selon une approche « zero-copy » sans duplication en mémoire, vous pourrez maintenir l'utilisation mémoire au repos comme en charge dans une fourchette de 22 Mo à 30 Mo.
Il faut également anticiper les environnements utilisateurs sous Windows. Pour les clients ne disposant pas du runtime WebView2, pensez à inclure le drapeau -webview2 download lors de la compilation afin d'embarquer le 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
`
Dans main.go, associez un http.Handler personnalisé à AssetServer.AssetsHandler pour éliminer les pics de consommation mémoire sur les gros volumes de données. Intégrez ensuite le pipeline ci-dessus dans .github/workflows/release.yml. À chaque création de tag, le binaire universel macOS ainsi que l'installateur Windows NSIS seront générés et publiés automatiquement sur GitHub Releases.
En prenant vous-même en charge la gestion de la mémoire à bas niveau et en lissant les différences entre les moteurs webview directement dans le code, vous serez en mesure de développer des applications de bureau d'une solidité exemplaire avec Wails.