Als mir jemand Tauri zum ersten Mal beschrieb, sagte er, man baue damit eine Desktop-App aus HTML, CSS und JavaScript. Meine Reaktion war Skepsis. Das klingt nach einer Website im Fenster, nicht nach einer echten Anwendung. Dann wies er darauf hin, dass VS Code, Slack und Discord alle auf dieselbe Weise gebaut sind, mit Electron, dem älteren und populäreren Vertreter dieses Ansatzes. Das sind echte Anwendungen, die jeden Tag von Millionen Menschen genutzt werden. Die Architektur hat einen Namen: hybride Desktop-Apps. Das ist kein Hack, und es ist nicht neu.
Es läuft so ab:
UI-Schicht (Web-Technologien)
↓
Native Bridge
↓
OS-FähigkeitenDeine Oberfläche ist mit der Web-Plattform gebaut. Eine Bridge verbindet sie mit dem Betriebssystem. Die Bridge ist es, die eine Webseite in eine Desktop-Anwendung verwandelt.
Electron hat bewiesen, dass das funktioniert
Du hast wahrscheinlich Electron-Apps genutzt, ohne es zu wissen. VS Code, Slack, Discord und eine lange Liste weiterer Desktop-Tools sind alle damit gebaut. Electron vertrat die Position, dass das Bündeln einer kompletten Browser-Engine in jeder App ein fairer Preis dafür sei, Web-Entwicklern den Versand von Desktop-Software ohne das Erlernen eines neuen Stacks zu ermöglichen.
HTML / CSS / JS
↓
Chromium + Node-Bridge
↓
OSDer Kompromiss ist die Größe. Jede Electron-App liefert ihre eigene Kopie von Chromium und Node mit. Eine kleine Electron-App kann auf der Festplatte 150 Megabyte belegen, weil sie einen ganzen Browser enthält. Der Speicherverbrauch folgt demselben Muster: Jedes Electron-Fenster bringt seinen eigenen Chromium-Prozess mit.
Tauri vertritt dieselbe Philosophie, geht aber andere Kompromisse ein:
HTML / CSS / JS
↓
Tauri IPC
↓
Rust
↓
OSAnstatt einen Browser mitzuliefern, nutzt Tauri den WebView, der bereits auf dem System des Nutzers installiert ist. Auf Windows ist das WebView2, auf macOS ist es WKWebView, auf Linux ist es WebKitGTK. Anstelle von Node ist das Backend Rust.
| Electron | Tauri | |
|---|---|---|
| Browser-Engine | Mitgeliefertes Chromium | System-WebView |
| Backend | Node.js | Rust |
| App-Größe | Groß (100+ MB) | Klein (typisch 2-10 MB) |
| Sicherheitsmodell | Node hat vollen Systemzugriff | Frontend ist standardmäßig unvertraut |
| Speicher | Höher (volles Chromium pro App) | Niedriger (teilt System-WebView) |
Der Größenunterschied ist, was den meisten Leuten zuerst auffällt. Das Sicherheitsmodell ist die interessantere Unterscheidung, und dazu komme ich gleich.
Ein Prozess, nicht zwei
Als ich anfing, über Tauri zu lesen, suchte ich ständig nach der Stelle, an der das Frontend mit dem Backend verbunden wird. Ich hatte das Web-Modell im Kopf, bei dem der Browser über HTTP mit einem Server spricht, und ging davon aus, Tauri hätte etwas Ähnliches. Eine Localhost-API vielleicht, oder eine Art eingebetteter Server, mit dem der WebView kommuniziert.
So läuft es nicht.
Wenn man ausführt:
pnpm tauri devstartet eine Rust-Executable. Diese Executable macht vier Dinge:
- Erzeugt ein natives OS-Fenster
- Erzeugt einen WebView innerhalb dieses Fensters
- Lädt dein Frontend in den WebView
- Erzeugt eine IPC-Bridge zwischen dem WebView und dem Rust-Code
Rust-Executable
├── natives Fenster
├── WebView (deine React/Vue/Svelte-App)
└── IPC-BridgeEs gibt keinen separaten Frontend-Server. Es gibt keine Localhost-API. Es gibt keinen separaten Backend-Prozess. Der Rust-Prozess ist der Orchestrator. Er erzeugt das Fenster, lädt die Web-UI hinein und steht bereit, Nachrichten von ihr zu verarbeiten.
Das war der Teil, mit dem ich eine Weile sitzen musste. In einer Web-App sind Browser und Backend zwei getrennte Programme, die auf verschiedenen Maschinen laufen. Der Browser findet das Backend über eine URL. In Tauri sind sie ein Programm. Das "Backend" ist nur der Rust-Code, der in demselben Prozess läuft, der den WebView überhaupt erst erzeugt hat. Wenn du Next.js genutzt hast, ist das näher daran. Dein Frontend und deine API-Routen sind ein Projekt, ein Prozess, und das Framework übernimmt das Routing. Du deployst kein getrenntes Frontend und Backend. Tauri hat dieselbe Form.
Wie JavaScript mit Rust spricht
Aus der Web-Entwicklung heraus würde man erwarten, einfach etwas zu importieren und aufzurufen. So funktioniert alles in JavaScript. Man installiert ein Paket, man importiert es, man nutzt es.
Tauri gibt dir einen Helfer, der genau so aussieht:
import { invoke } from "@tauri-apps/api/core";
const result = await invoke("greet", { name: "John" });Und an der Oberfläche ist es das auch. Man ruft etwas auf, man bekommt ein Resultat zurück. Aber darunter funktioniert nichts davon so, wie man es aus dem Web kennt. Es gibt kein gemeinsames Modul. Es gibt keine importierte Funktion. Man sendet eine Nachricht an einen anderen Prozess und wartet darauf, dass er antwortet.
JavaScript
|
| Nachricht: { command: "greet", args: { name: "John" } }
|
IPC-Bridge
|
Rust-Command-Dispatcher
|
fn greet(name: String) -> String
|
| gibt zurück: "Hello, John"
|
JavaScript Promise resolvedAuf der Rust-Seite sieht die Funktion, die das empfängt, so aus:
#[tauri::command]
fn greet(name: String) -> String {
format!("Hello, {}", name)
}Das Attribut #[tauri::command] registriert die Funktion beim IPC-Dispatcher. Wenn JavaScript invoke("greet", ...) aufruft, findet der Dispatcher diese Funktion, übergibt die Argumente und gibt das Resultat zurück. Die JavaScript-Seite erhält ein Promise. Die Rust-Seite führt eine echte kompilierte Funktion aus.
Der Name dafür ist IPC, Inter-Process Communication. Der Grund, warum es existiert, ist, dass zwei Prozesse nicht direkt in den Speicher des anderen greifen können. Sie müssen über Nachrichten kommunizieren. Was man oben sieht, ist ein strukturierter Weg für zwei Prozesse, Daten auszutauschen, ohne Speicher zu teilen.
Es gab einen Teil dabei, der mich stolpern ließ. In einer Web-App spricht der Browser über HTTP mit dem Backend, was bedeutet, dass er einen Port braucht, etwas wie localhost:3000. In Tauri gibt es keinen Port. Ich suchte ständig danach, wo die Verbindung eingerichtet wird, und konnte sie nicht finden, weil es keine Verbindung gibt. Die IPC-Bridge wird intern von der Tauri-Runtime erzeugt. Es ist wie zwei Räume im selben Gebäude. Man ruft jemanden nicht an, wenn er im Nebenraum sitzt. Man benutzt die interne Rohrpost.
Die Bridge ist schon da
Als ich verstanden hatte, dass es keinen Port gibt, war die nächste Frage naheliegend: wenn es keinen Server gibt, wie findet das JavaScript das Rust?
Die Antwort ist, dass es es nicht findet. Es muss es nicht finden.
Wenn der Rust-Prozess den WebView erzeugt, injiziert er die Kommunikations-Bridge hinein. Konzeptionell:
Rust-Runtime
|
| injiziert
|
window.__TAURI__Wenn dein JavaScript invoke() aufruft, nutzt es diese Bridge. Das Frontend findet Rust nicht. Es gibt keine URL, auf die man zeigen könnte, keinen Port, den man verbinden könnte. Rust hat die Umgebung gebaut, in der das Frontend läuft, und der Kommunikationskanal war Teil dieser Umgebung, bevor die erste Zeile JavaScript geladen wurde.
Das ist ein sauberer Unterschied zum Web-Modell. Im Web findet der Browser das Backend über eine URL. In Tauri hat das Backend die Umgebung gebaut, in der das Frontend läuft, und der Kommunikationskanal war von Anfang an Teil dieser Umgebung.
Wo die Vertrauensgrenze liegt
Das Frontend ist unvertraut. Das gilt für jedes Frontend, und es ist nicht der interessante Teil. Der interessante Teil ist, wo jedes Framework die Vertrauensgrenze zieht und was es dir erlaubt zu scopen.
In Electron hat das Node-Backend standardmäßig vollen Systemzugriff. Ob das Frontend diesen Zugriff bekommt, ist eine Config-Flag, nodeIntegration. Setze sie auf true und dein Renderer kann child_process, fs, alles, was Node kann, aufrufen. Setze sie auf false und der Renderer ist sandboxed, aber der Main-Prozess hat weiterhin vollen Zugriff, und du exponierst typischerweise, was du brauchst, über ipcMain.handle. Die Grenze lautet "hat das Frontend die Node-API oder nicht." Das ist ein binäres Gate. Es gibt kein Capability-System, das sagt, das Frontend darf exportReport aufrufen, aber nicht deleteFile. Diese Logik schreibt man selbst in jeden IPC-Handler.
In Tauri hat das Frontend standardmäßig keinen Zugriff. Jedes Command, das das Frontend aufrufen kann, muss explizit als #[tauri::command] registriert sein, und das Capability-System kontrolliert, welche Commands das Frontend aufrufen darf. Nur die Rust-Seite hat OS-Zugriff. Das Capability-System scopen, was das Frontend anfragen kann. Du schreibst keinen Guard in jeden Handler. Das Framework erzwingt es.
| Electron | Tauri | |
|---|---|---|
| Backend-OS-Zugriff | Standardmäßig voll | Voll, aber nur Rust hat ihn |
| Frontend-Zugriff auf OS | Config-Flag (nodeIntegration) | Standardmäßig keiner |
| Was das Frontend aufrufen kann | Was auch immer du über ipcMain.handle exponierst | Nur Commands, die mit #[tauri::command] registriert sind |
| Scoping pro Command | Du schreibst die Guard-Logik selbst | Capability-System scopen jedes Command |
Die Restaurant-Analogie hält immer noch. Das Service-Personal im Gastraum legt Bestellungen auf einen Ticketschienen. Die Küche entscheidet, was gemacht wird und wie. Aber in Electron hat die Küchentür ein Schloss, und man muss sich daran erinnern, es zu schließen, und entscheiden, wer einen Schlüssel bekommt. In Tauri beginnt die Küchentür verschlossen, und jedes spezifische Gericht, das der Gastraum bestellen kann, muss explizit zur Speisekarte hinzugefügt werden. Der Standard lautet "nichts ist auf der Karte." Man fügt Elemente nacheinander hinzu, und das Framework erzwingt es.
Der Build
Während der Entwicklung laufen zwei Dinge gleichzeitig:
Frontend: Vite Dev-Server (Hot Reload, Fast Refresh)
Backend: Cargo (Rust-Compiler, überwacht Änderungen)Du bearbeitest deine React-Komponenten, Vite lädt sie hot neu. Du bearbeitest deinen Rust-Code, Cargo kompiliert neu. Das native Fenster bleibt offen, während sich beide Seiten aktualisieren.
In Produktion wird das Frontend gebaut und das Rust wird zu einem Release-Binary kompiliert. Beide werden zusammengepackt:
Frontend-Build
+
Rust-Release-Build
|
v
Native AnwendungDer Output hängt vom Target ab:
| Plattform | Output |
|---|---|
| Windows | .exe / .msi |
| macOS | .app / .dmg |
| Linux | AppImage / .deb |
| Android | .apk / .aab |
| iOS | .ipa |
Tauri Mobile ist hier erwähnenswert. Auf Mobile ist die Architektur dieselbe: Deine Web-UI läuft im WebView der Plattform, Android WebView oder iOS WKWebView, und Rust wird nativ für die mobile Plattform kompiliert. Es ist nicht React Native. Es gibt keine JavaScript-zu-Native-UI-Bridge. Die UI ist ein WebView, und Rust übernimmt die native Seite.
Browser vs Tauri
Deine React-App kann mit pnpm dev in einem normalen Browser laufen. Aber invoke() wird nicht funktionieren, weil es keine Rust-Runtime und keine IPC-Bridge gibt. Die Funktion existiert im JavaScript-Paket, aber wenn sie versucht, eine Nachricht über die Bridge zu senden, ist am anderen Ende nichts.
In Tauri läuft die Rust-Runtime, die Bridge existiert, und invoke() transportiert Nachrichten an echte Rust-Funktionen. Das ist der Unterschied zwischen dem Öffnen deiner App in Chrome und dem Ausführen über Tauri. Der Code ist derselbe. Die Runtime ist anders.
Wann es passt
Tauri funktioniert gut für:
- Desktop-Produktivitäts-Apps
- Entwickler-Tools
- Business-Software
- Notiz-Apps
- Datei-Utilities
- interne Firmen-Tools
- Cross-Platform-Utilities, die auf Windows, macOS und Linux laufen müssen
Es ist weniger ideal für:
- Hochleistungs-3D-Spiele
- Grafiklastige Anwendungen, bei denen man jedes native UI-Detail braucht
- Apps, die tiefe plattformspezifische Integration mit OS-Level-UI-Frameworks benötigen
Die guten Fälle teilen sich etwas: Die Oberfläche ist die Arbeit, und das Betriebssystem stellt unterstützende Fähigkeiten zur Verfügung wie Dateizugriff, Benachrichtigungen und Fenstermanagement. Die schlechten Fälle sind das Gegenteil: Das Grafik- oder UI-System der nativen Plattform ist die Arbeit, und eine Web-UI steht im Weg.
Der Denkwechsel
Die Frage, mit der ich begonnen habe, war, ob wir wirklich Oberflächen entwerfen statt Desktop-Apps. Die Antwort ist ja, aber die Rahmung zählt.
Man packt keine Website in ein Desktop-Fenster. Man baut eine native Anwendung, bei der die UI-Technologie zufällig die Web-Plattform ist. Diese Unterscheidung ist es, was die Architektur funktioniert. Die Rust-Seite ist ein echtes natives Backend mit echtem OS-Zugriff. Die Web-Seite ist die Präsentationsschicht. Die Bridge dazwischen ist, wo die Vertrauensgrenze liegt.
Eine Website im Fenster wäre ein Browser, der auf eine URL zeigt, ohne native Fähigkeiten. Was Tauri einem gibt, ist eine kompilierte native Anwendung, die ihre Oberfläche zufällig mit Web-Technologien rendert. Der Unterschied ist das Rust-Backend, die IPC-Bridge und das Sicherheitsmodell darum herum.
Mehr lesen
Wie Mobile Apps funktionieren
Die drei Wege, eine Mobile-App zu bauen, was bei jedem unter der Haube tatsächlich läuft, und das gemeinsame Architektur-Muster hinter React Native, Flutter und Tauri Mobile.
Eine Codebase, Web und Desktop
Wie man eine Tauri-App so designt, dass derselbe React-Code im Browser und auf dem Desktop läuft, indem man die Runtime-Unterschiede hinter einem Service-Interface versteckt.
API-Schlüssel-Pooling mit Bifrost
Ein praktischer Leitfaden zur Konfiguration von API-Key-Pooling in Bifrost und zur sicheren Skalierung über mehrere Knoten durch Externalisierung der Rate Limits.
