Perché abbiamo abbandonato Electron
GitSquid v1 era costruito su Electron. Funzionava. Gli utenti potevano esplorare repository, preparare modifiche, gestire branch, gestire remote e usare un terminale integrato -- tutto da un'unica applicazione desktop. Sotto il cofano, ci affidavamo a simple-git per le operazioni Git, node-pty e xterm.js per il terminale, e il classico stack React per l'interfaccia utente.
Ma con la maturazione dell'app, i compromessi di Electron diventavano sempre più difficili da ignorare. Avevamo rilasciato un prodotto funzionante, eppure il runtime stesso lavorava contro di noi. Così abbiamo riscritto l'intero backend in Rust usando Tauri 2.x -- mantenendo il 95% del nostro frontend React intatto.
Questa è la storia del perché l'abbiamo fatto, come l'abbiamo fatto e cosa si è rotto lungo il percorso.
Il problema con Electron
Electron è un framework collaudato. VS Code, Slack, Discord -- grandi prodotti ci girano sopra. Ma per un client Git, l'overhead è sproporzionato rispetto al compito.
- Dimensione dell'app: Electron include un intero browser Chromium e un runtime Node.js. La nostra app impacchettata pesava oltre 150 MB. Per un client Git, è difficile da giustificare.
- Utilizzo della memoria: Ogni finestra Electron genera più processi Chromium. Il consumo di memoria a riposo era ben al di sopra di quello che un'app nativa richiederebbe per lo stesso lavoro.
- Tempo di avvio: Avviare Chromium non è istantaneo. Gli utenti notavano un ritardo percepibile prima che l'app diventasse interattiva, soprattutto su hardware più datato.
- Sensazione nativa: Nonostante gli sforzi di Electron con barre del titolo e menu nativi, l'app non dava mai veramente l'impressione di appartenere al sistema operativo. Piccoli dettagli -- gestione delle finestre, comportamento del focus, scorciatoie da tastiera -- avevano sempre un leggero sapore da web app.
Nessuno di questi è un problema decisivo preso singolarmente. Ma combinati, creavano un attrito che non si allineava con quello che uno strumento per sviluppatori dovrebbe trasmettere: velocità, leggerezza e invisibilità.
Perché Tauri 2.x
Abbiamo valutato diverse alternative prima di scegliere Tauri. I fattori decisivi erano chiari:
- Webview di sistema invece di Chromium integrato. Tauri usa la webview fornita dal sistema operativo (WebKit su macOS, WebView2 su Windows, WebKitGTK su Linux). Questo da solo elimina la maggior parte della dimensione binaria di Electron.
- Backend in Rust. Il backend gira come un binario Rust compilato. Sicurezza della memoria, nessun garbage collector, prestazioni prevedibili. Per uno strumento che esegue decine di operazioni Git per interazione, questo conta.
- Binari drasticamente più piccoli. Un'app Tauri può essere distribuita sotto i 10 MB. Confrontatelo con 150 MB+.
- Multi-piattaforma con integrazione nativa. Tauri 2.x fornisce API solide per menu nativi, system tray, notifiche e finestre di dialogo per i file. L'app si sente come se appartenesse al sistema operativo.
- Ecosistema attivo e buona esperienza di sviluppo. Il sistema di plugin di Tauri, il modello IPC e la documentazione sono maturati significativamente con il rilascio 2.x. Era pronto per l'uso in produzione.
La strategia di migrazione
Una riscrittura completa è rischiosa. Abbiamo mitigato quel rischio con una chiara separazione delle responsabilità.
Mantenere il frontend, riscrivere il backend
Il nostro frontend React era già strutturato attorno a un confine IPC. I componenti chiamavano funzioni che comunicavano con il processo principale di Electron; dovevamo semplicemente reindirizzare quelle chiamate all'API invoke di Tauri. Circa il 95% del codice React è stato trasferito senza modifiche.
Il backend è stata un'altra storia. Ogni servizio Node.js -- operazioni Git, monitoraggio dei file, gestione del terminale, autenticazione, impostazioni -- doveva essere reimplementato in Rust. In totale, abbiamo scritto circa 7.200 righe di Rust e migrato oltre 120 comandi IPC.
Git CLI invece di libgit2
Una domanda frequente: perché non usare git2-rs (i binding Rust per libgit2)?
Abbiamo scelto std::process::Command per chiamare direttamente la Git CLI. Le ragioni:
libgit2non supporta ogni funzionalità di Git. Operazioni come il rebase interattivo, alcune strategie di merge e certe opzioni di configurazione sono assenti o si comportano diversamente.- Gli utenti hanno già Git installato. Chiamando il loro binario Git locale, garantiamo parità di comportamento con la riga di comando. Se funziona nel terminale, funziona in GitSquid.
- Il debugging è più semplice. I comandi che eseguiamo sono gli stessi che un utente digiterebbe. Quando qualcosa va storto, i messaggi di errore sono familiari.
Sostituire l'ecosistema Node.js
Ogni dipendenza Node.js è stata sostituita con un equivalente Rust:
simple-gitè diventato chiamate dirette alla Git CLI viastd::process::Commandnode-ptyè diventatoportable-ptyper l'emulazione del terminale- L'archiviazione dei token è passata al crate
keyring, che usa il portachiavi del sistema operativo (macOS Keychain, Windows Credential Manager, Linux Secret Service) - Il monitoraggio dei file è passato al crate
notify - Le richieste HTTP sono passate a
reqwest
Migrazione delle impostazioni senza interruzioni
Abbiamo mantenuto la stessa struttura della directory di configurazione. Gli utenti che aggiornano da v1 a v2 non perdono le loro impostazioni, i repository salvati o le preferenze. Il backend Rust legge gli stessi file di configurazione scritti dal backend Node.js.
Le sfide che abbiamo affrontato
La migrazione non è stata una passeggiata. Diversi problemi ci sono costati tempo significativo di debugging.
Byte nulli nel parsing di Git log
Usiamo stringhe di formato personalizzate con git log --format per il parsing dei dati dei commit. Nella versione Electron, usavamo caratteri separatori specifici tra i campi. Nel porting a Rust, inizialmente abbiamo provato a usare byte nulli (\x00) come separatori di campo -- sembravano una scelta sicura dato che non sarebbero mai apparsi nei messaggi di commit.
Sbagliato. Le stringhe Rust gestiscono i byte nulli senza problemi, ma diversi livelli dello stack li trattano come terminatori di stringa in stile C. L'output del log veniva troncato silenziosamente al primo byte nullo. Siamo passati a un delimitatore multi-carattere che non sarebbe mai apparso naturalmente nell'output di Git.
Archiviazione dei token nel portachiavi
Il crate keyring archivia le credenziali nel portachiavi del sistema operativo, che è l'approccio corretto. Ma ci siamo imbattuti in un problema simile con i byte nulli: stavamo concatenando i dati dei token con separatori a byte nulli prima di archiviarli. Al recupero, alcune implementazioni del portachiavi del sistema operativo troncavano il valore al primo byte nullo, restituendo credenziali incomplete. La soluzione è stata la stessa -- usare un separatore visibile e non ambiguo.
Complessità dell'autenticazione BitBucket
GitHub e GitLab hanno flussi di autenticazione basati su token relativamente semplici. BitBucket è più complesso. A seconda dell'operazione e del tipo di account, potresti aver bisogno di un nome utente, un'email, una password dell'app o un token OAuth -- e le regole su quale usare e dove non sono sempre ovvie. Far funzionare in modo affidabile l'autenticazione BitBucket per clone, fetch, push e chiamate API ha richiesto diverse iterazioni.
Staging per singola riga
GitSquid supporta lo staging di singole righe da un diff, non solo di interi hunk. Nella versione Electron, la generazione delle patch avveniva in JavaScript. Per la versione Tauri, abbiamo spostato questa logica nel backend Rust. Generare un formato patch Git valido da selezioni arbitrarie di righe, gestire i casi limite con le righe di contesto e assicurarsi che git apply accetti il risultato -- questa è stata una delle parti più intricate della riscrittura. L'implementazione Rust si è rivelata più robusta della versione originale in JavaScript.
Apostrofi francesi che rompevano le build
Questo caso è stato memorabile. Alcune delle nostre stringhe visibili all'utente contenevano apostrofi tipografici francesi (la varietà curva, U+2019) invece di apostrofi ASCII dritti. Questi causavano problemi di encoding durante il processo di build in certe configurazioni CI. La soluzione è stata banale una volta identificata, ma i messaggi di errore non puntavano minimamente al problema reale.
Risultati
Dopo aver completato la migrazione e raggiunto la piena parità di funzionalità, ecco dove siamo arrivati:
- Dimensione dell'app: significativamente ridotta rispetto alla build Electron. Il bundle Tauri è una frazione dei 150 MB+ che distribuivamo prima.
- Utilizzo della memoria: baseline più bassa e più prevedibile sotto carico. Nessun albero di processi Chromium che consuma RAM in background.
- Tempo di avvio: notevolmente più veloce. L'app è interattiva quasi immediatamente.
- Parità di funzionalità: 100%. Ogni funzionalità di v1 esiste in v2. Il terminale integrato, lo staging per riga, il supporto multi-remote, la gestione dei branch -- tutto.
- Riutilizzo del frontend: il 95% del codice React è stato trasferito, validando la nostra decisione architettonica di mantenere un confine IPC pulito.
Ne è valsa la pena?
Sì.
La riscrittura ha richiesto uno sforzo significativo. Circa 7.200 righe di Rust, più di 120 comandi IPC da reimplementare e diversi bug non ovvi che sono emersi solo perché Rust e Node.js gestiscono i casi limite in modo diverso. Non è stato un progetto da weekend.
Ma il risultato è un'applicazione fondamentalmente migliore. GitSquid v2 si avvia più velocemente, usa meno memoria, si distribuisce come un binario più piccolo e si sente più nativo su ogni piattaforma. Il backend Rust è più prevedibile e più facile da ragionare rispetto all'equivalente Node.js. E poiché abbiamo mantenuto lo stesso frontend React, gli utenti vedono la stessa interfaccia che già conoscono.
Se stai mantenendo un'app Electron e l'overhead del runtime ti disturba, Tauri è un'opzione seria. L'ecosistema è abbastanza maturo per l'uso in produzione, e il percorso di migrazione -- specialmente se hai già una separazione pulita tra frontend e backend -- è più gestibile di quanto una riscrittura completa potrebbe suggerire.
L'intuizione chiave: tratta il tuo confine IPC come un contratto API. Se il tuo frontend sa solo come fare invoke('get_commits', { path, branch }) e non gli importa se la risposta arriva da Node.js o Rust, sostituire il backend diventa un progetto realistico invece di una fantasia.