Torna al blog

06 / scrittura

Quando il web non basta: il protocollo bridge di Wails

Perché alcuni software devono uscire dal browser — e come un bridge sottile consente di mantenere UI di qualità web con codice nativo reale sottostante.

Il web ha vinto — tranne dove non l’ha fatto

Per la maggior parte dei software, il browser è la risposta giusta. Un’app web non installa nulla, si aggiorna da sola, gira su ogni sistema operativo e ti permette di costruire l’interfaccia con l’ecosistema front-end più ricco e meglio attrezzato del mondo. Se puoi consegnare un’app web, di solito dovresti farlo.

Ma il browser è una sandbox, e quella sandbox ha dei muri. Esistono per buone ragioni — una pagina web casuale non dovrebbe poter leggere il flusso di loopback grezzo del microfono, aprire file arbitrari, parlare con un dispositivo USB, o eseguire un ciclo numerico stretto che blocca un core della CPU. Le stesse restrizioni che rendono il web sicuro da navigare lo rendono lo strumento sbagliato per un’intera classe di programmi.

Quando si colpisce uno di quei muri, non si vuole davvero buttare via l’interfaccia web. Lo stack di rendering HTML/CSS/JS è ottimo. Quello che si vuole è mantenere la dashboard e collegarla a un motore che gira fuori dalla sandbox — un vero processo nativo, in un linguaggio adatto a ciò che il browser non ti lascia fare.

Quella giuntura tra “UI web” e “motore nativo” è il bridge. Questo post parla di cosa sia effettivamente un protocollo bridge, e della regola decisionale per capire quando se ne ha bisogno.


Quando un programma web non è lo strumento giusto

Quattro segnali, ognuno dei quali dovrebbe spingerti oltre il browser.

1. Hai bisogno di hardware che la sandbox non cede

I browser espongono una fetta curata e con permessi della macchina: una fotocamera, un ingresso microfono, forse un gamepad o un singolo dispositivo USB tramite WebUSB se l’utente clicca su un prompt. Quello che deliberatamente non forniscono è la roba complicata e potente:

  • Audio di sistema / loopback — “registra quello che sento”, non solo “registra il mio microfono”. La Web Audio API può catturare un microfono; non può intercettare in modo affidabile l’uscita audio dell’OS. Un registratore di riunioni, uno strumento di trascrizione, un router audio — tutti hanno bisogno dell’API audio nativa del sistema operativo.
  • Accesso arbitrario al filesystem — monitorare una directory, mappare in memoria un file grande, gestire un database locale su disco.
  • Porte seriali, stack Bluetooth, stampanti, scanner, strumenti scientifici — periferiche i cui driver vivono in C e parlano protocolli che il browser non ha mai modellato.
  • Hotkey globali, icone nel tray, gestione finestre, avvio automatico — integrazione OS che una scheda semplicemente non è autorizzata a fare.

Se la lista delle funzionalità contiene una periferica o una capacità OS che la sandbox blocca, questo è il segnale più chiaro di tutti. Nessuna quantità di JavaScript intelligente ti fa passare un muro che esiste di proposito.

2. Hai bisogno di calcolo grezzo, in un linguaggio costruito per farlo

JavaScript in un browser è veloce — finché non lo è. Per la logica UI e l’I/O va benissimo. Ma per calcoli numerici sostenuti paga un vero pedaggio: un event loop single-thread, pause da garbage collection, riscaldamento JIT, e nessun vero parallelismo a memoria condivisa senza saltare attraverso cerchi di Web Worker / WASM.

Quando il nucleo del programma è computazione — elaborazione del segnale, simulazione, crittografia, parsing di gigabyte, inferenza in tempo reale, qualsiasi cosa che voglia tutti i core della macchina — si vuole un linguaggio progettato per questo: Go, Rust, C, C++, Zig. Compilato, multi-thread, memoria prevedibile, accesso diretto a SIMD e librerie native. Si sposta il ciclo pesante in quel linguaggio e si lascia che il layer web rimanga una skin di presentazione sottile sopra di esso.

Il segnale: il tuo tetto di prestazioni è un problema CPU, non un problema rete. Se rendere il lavoro più veloce significa “usa più core / meno GC / memoria più compatta,” il browser è il runtime sbagliato per quel lavoro.

3. Deve funzionare senza un server — privacy, latenza, o offline

Un’app web, quasi per definizione, chiama casa. Per alcuni software questo è inaccettabile:

  • Privacy — i dati non devono mai lasciare la macchina. Note mediche, materiale legale, audio grezzo di riunioni, qualsiasi cosa che preferiresti non trasmettere al cloud di qualcun altro. Il codice nativo può fare il lavoro sensibile localmente e inviare solo ciò che l’utente sceglie esplicitamente.
  • Latenza — un round trip verso un server sono decine o centinaia di millisecondi che non hai se stai reagendo a un input live in tempo reale.
  • Offline — deve continuare a funzionare su un aereo, in un seminterrato ospedaliero, dietro un firewall aziendale. Nessuna connessione, nessun problema.

Se “inviarlo a un server” non è un’opzione per una qualsiasi di quelle ragioni, la logica deve vivere sulla macchina — il che significa un programma locale, non una pagina.

4. Deve essere una cosa sola che l’utente può semplicemente eseguire

C’è anche un fattore umano. Uno stack web spesso significa “installa Node, avvia due server, imposta alcune variabili d’ambiente, tieni un terminale aperto.” Per uno sviluppatore è routine. Per tutti gli altri è un muro.

Un’app nativa può essere distribuita come un unico eseguibile: scarica un file, fai doppio clic, funziona. Nessun runtime da installare, nessuna porta da ricordare, nessun localhost da tenere attivo. Quando l’utente non è un sysadmin — e specialmente quando usa lo strumento sotto pressione — “un file che funziona” è di per sé una funzionalità.


Quindi hai deciso di passare al nativo. E adesso?

La via nativa ingenua consiste nel costruire anche l’interfaccia in modo nativo — Qt, GTK, Cocoa, WinUI. Potente, ma rinunci alla velocità di design del web e devi ricostruire per piattaforma.

La risposta moderna è un ibrido: renderizzare l’interfaccia con tecnologia web all’interno di una finestra nativa, ed eseguire la logica come un binario nativo compilato. Electron ha popolarizzato questo, ma lo paga includendo un intero runtime Chromium + Node — centinaia di megabyte — in ogni app.

L’approccio più leggero usa il proprio webview del sistema operativo (WebKit su macOS, WebView2 su Windows, WebKitGTK su Linux) per il rendering, e un singolo binario compilato per la logica. Nessun browser incluso, nessun Node incluso. Questo è il modello dietro framework come Wails (Go) e Tauri (Rust). Leggero, veloce, nativo — con un’interfaccia web sopra.

E nel momento in cui si divide il programma in “UI web” e “motore nativo,” è necessario un modo per le due metà di comunicare. Quel layer di comunicazione — il suo meccanismo, le sue convenzioni, le sue regole — è il protocollo bridge.


Cosa sia effettivamente un protocollo bridge

Un bridge non è un’API di rete. Non c’è socket, porta, HTTP tra le due metà — sono nello stesso processo, ai lati opposti del confine del webview. Il bridge è l’insieme di regole per trasportare chiamate e dati attraverso quel confine. Ogni framework ibrido ne ha uno, e tutti risolvono gli stessi due problemi:

Direzione A — L’interfaccia chiama il codice nativo (richiesta/risposta)

Si scrive una funzione nel linguaggio nativo. Il framework la riflette e genera uno stub JavaScript corrispondente. L’interfaccia chiama lo stub come qualsiasi funzione asincrona; il framework serializza gli argomenti (di solito in JSON), li trasporta attraverso il confine, esegue la vera funzione sul lato nativo, e risolve la promise JS con il risultato.

Nativo:  func Login(email, password string) (string, error)
                        │   (il framework riflette + genera un binding)
                        ▼
JS:      App.Login(email, password)   // restituisce Promise<string>

Questa è la direzione pull: l’interfaccia chiede, il motore risponde. Si noti come pulitamente un linguaggio che restituisce (value, error) si mappa su resolve / reject di JavaScript — il bridge è in gran parte un layer di traduzione tra le convenzioni di chiamata di due linguaggi. L’interfaccia non pensa mai a JSON o al confine; chiama semplicemente “una funzione.”

Direzione B — Il codice nativo spinge verso l’interfaccia (eventi)

Richiesta/risposta va bene per “recupera questi dati.” Ma i motori nativi spesso producono stream — un misuratore del livello audio che scatta 30 volte al secondo, il progresso su un calcolo lungo, la coda di un log, la lettura di un sensore. Per queste, il motore spinge piuttosto che aspettare di essere interrogato.

Ogni framework bridge fornisce un event bus per questo: il lato nativo emette un evento con nome e un payload; l’interfaccia si iscrive a quel nome con una callback e si cancella quando il componente sparisce.

Nativo:  emit("level:mic", amplitude)   ──▶   UI:  on("level:mic", updateMeter)
Nativo:  emit("progress", 0.42)         ──▶   UI:  on("progress", setProgressBar)

Questa è la direzione push: il motore parla, l’interfaccia ascolta.

L’intero protocollo in una regola

Pull per comandi e query; push per stream.

Tutto qui. Binding di funzioni generate in una direzione, un event bus con nome nell’altra. Due meccanismi, un confine. Una volta interiorizzata quella divisione, ogni framework desktop ibrido sembra uguale sotto il cofano — i nomi cambiano (Bind vs. invoke, EventsEmit vs. emit), ma la forma è identica.

  UI  (web, nel webview dell'OS)                  MOTORE  (binario nativo compilato)
  ─────────────────────────────                   ─────────────────────────────────
  call("StartRecording")  ───────────────▶        func StartRecording()
        (promise risolta) ◀─────────────           return ok / error

  on("level:mic", setMeter) ◀────────────          emit("level:mic", db)
  on("transcript", append)  ◀────────────          emit("transcript", segment)

Un pattern di design da rubare: il volante unico

Una convenzione sottile ma importante emerge nei bridge ben costruiti. Non si espongono decine di oggetti nativi non correlati all’interfaccia. Se ne espone uno — una singola struttura facade i cui metodi sono l’intera superficie API dell’app. Il framework riflette su quell’unico oggetto e genera l’intero set di binding da esso.

Quell’unico oggetto è il volante. L’interfaccia lo gira; dietro di esso, la facade delega a moduli interni focalizzati (un modulo audio, un modulo di trascrizione, un modulo di storage) che l’interfaccia non vede mai direttamente. Il vantaggio:

  • Un’API pulita e denominata — l’interfaccia pensa in verbi (StartRecording, Login, ListFiles), non nelle tubature disordinate dietro ognuno.
  • Un singolo punto di audit — tutto ciò che attraversa il confine passa attraverso un’unica superficie, quindi è facile ragionare su ciò che l’interfaccia può e non può attivare.
  • Libertà di refactoring — si possono riscrivere completamente gli interni del motore finché le firme dei metodi della facade reggono.

È il pattern Facade, applicato a un confine di processo. Il bridge fornisce il trasporto; la convenzione a singola facade fornisce un contratto pulito da mettere sopra.


Un esempio pratico: un registratore di riunioni / interviste

Per rendere tutto questo concreto, immagina uno strumento che registra una conversazione live, la trascrive al volo e offre aiuto al momento — il tipo di cosa che questa codebase si trova ad essere. Applica i quattro segnali:

  1. Hardware? Sì — deve catturare audio di sistema (l’altro lato della chiamata) più il microfono. Il browser non può fare in modo affidabile la cattura del loopback. → API audio nativa richiesta.
  2. Calcolo? Audio in tempo reale: rilevamento dell’attività vocale che taglia lo stream sul silenzio, buffering PCM, trasmissione di chunk a un motore di trascrizione — tutto lavoro sostenuto, a bassa latenza che vuole un linguaggio compilato e concorrente.
  3. Senza server? L’audio di una riunione live è sensibile; gli utenti vogliono l’opzione di trascrivere localmente e tenerlo offline. → logica sulla macchina.
  4. Basta eseguirlo? L’utente è nel mezzo di un colloquio, non sta configurando un ambiente di sviluppo. → un singolo binario cliccabile.

Quattro su quattro. Questo è un caso di testo per passare al nativo — e per il bridge. L’interfaccia web mostra la trascrizione live, i misuratori di livello, i controlli. Il motore nativo apre due stream audio, esegue VAD su thread reali, parla con un backend di trascrizione, e spinge un evento transcript per ogni segmento finito e un evento level:mic molte volte al secondo. L’interfaccia tira con StartRecording / StopRecording. Pull per comandi, push per stream — la stessa regola, che fa lavoro reale.

Al contrario, un CMS blog o un tracker di progetto: nessun hardware speciale, nessun calcolo pesante, basato su server per design, e il “niente da installare” del browser è la funzionalità. Quella è un’app web, punto. Il bridge serve all’altra categoria — e ora si può capire in quale categoria ci si trova.


La decisione, in una tabella

Se il tuo programma……il web è…perché
Ha bisogno di audio loopback, serial/USB, monitoraggio filesystem, hook OSnon abbastanzala sandbox blocca quelle cose di proposito
Vive o muore sul calcolo CPU-boundnon abbastanzavuoi codice compilato, multi-thread, leggero su GC
Deve girare offline / completamente locale / privacy-firstnon abbastanzauna pagina assume un server; la logica nativa no
Deve essere distribuito come un unico file cliccabilenon abbastanza“nessuna installazione” batte “configura un runtime”
È CRUD su una rete, su ogni dispositivo, senza installazioneesattamente giustola sandbox non ti costa nulla qui

Quando si finisce in una delle prime quattro righe, si opta per un’app locale nativa — e il bridge è ciò che ti permette di farlo senza rinunciare all’interfaccia web che avresti costruito comunque. Si scrivono le parti difficili in un linguaggio adatto alle parti difficili, si scrive l’interfaccia nel linguaggio adatto alle interfacce, e un protocollo sottile a due direzioni — chiamate generate in un senso, un event bus nell’altro — le unisce in un singolo programma che l’utente fa doppio clic.

Questa è l’intera idea. Il web non ha perso; ha solo dei bordi. Il bridge è il modo in cui si costruisce proprio fino ad essi.


Framework che vale la pena esplorare se vuoi provare questo: Wails (Go + webview), Tauri (Rust + webview). Entrambi implementano esattamente il protocollo a due direzioni descritto qui.


Letture consigliate


L’autore

Ivens Signorini è un Senior Backend Engineer specializzato in sistemi distribuiti, infrastruttura AI e API ad alte prestazioni. Lavora principalmente in Go e TypeScript, costruendo sistemi che operano su larga scala. I suoi interessi tecnici includono il design dei protocolli, i pattern di concorrenza e l’architettura delle applicazioni AI-native. Scrive su signorini.cloud.

Tweaks

Accent
Theme
Headline font