← Zurück zum Blog

Warum wir unsere Electron-App in Tauri/Rust umgeschrieben haben

engineering tauri rust

Warum wir Electron hinter uns gelassen haben

GitSquid v1 basierte auf Electron. Es funktionierte. Nutzer konnten Repositories durchsuchen, Änderungen stagen, Branches verwalten, Remotes handhaben und ein integriertes Terminal nutzen -- alles in einer einzigen Desktop-App. Unter der Haube setzten wir auf simple-git für Git-Operationen, node-pty und xterm.js für das Terminal sowie den üblichen React-Stack für die Benutzeroberfläche.

Doch mit zunehmender Reife der App ließen sich die Kompromisse von Electron immer schwerer ignorieren. Wir hatten ein funktionierendes Produkt ausgeliefert, aber die Runtime selbst arbeitete gegen uns. Also haben wir das gesamte Backend in Rust mit Tauri 2.x neu geschrieben -- und dabei 95 % unseres React-Frontends beibehalten.

Dies ist die Geschichte, warum wir es getan haben, wie wir es getan haben und was dabei schiefgegangen ist.

Das Problem mit Electron

Electron ist ein bewährtes Framework. VS Code, Slack, Discord -- große Produkte laufen darauf. Aber für einen Git-Client ist der Overhead im Verhältnis zur Aufgabe unverhältnismäßig.

  • App-Größe: Electron bündelt einen kompletten Chromium-Browser und eine Node.js-Runtime. Unsere gepackte App wog über 150 MB. Für einen Git-Client ist das schwer zu rechtfertigen.
  • Speicherverbrauch: Jedes Electron-Fenster startet mehrere Chromium-Prozesse. Der Speicherverbrauch im Leerlauf lag deutlich über dem, was eine native App für dieselbe Arbeit benötigen würde.
  • Startzeit: Chromium startet nicht sofort. Nutzer bemerkten eine spürbare Verzögerung, bevor die App interaktiv wurde, besonders auf älterer Hardware.
  • Natives Gefühl: Trotz Electrons Bemühungen mit nativen Titelleisten und Menüs fühlte sich die App nie so an, als gehöre sie zum Betriebssystem. Kleinigkeiten -- Fensterverwaltung, Fokus-Verhalten, Tastaturkürzel -- hatten immer einen leichten Web-App-Beigeschmack.

Nichts davon ist einzeln betrachtet ein Ausschlusskriterium. Aber zusammen erzeugten sie eine Reibung, die nicht zu dem passte, wie sich ein Entwicklertool anfühlen sollte: schnell, leichtgewichtig und unsichtbar.

Warum Tauri 2.x

Wir haben mehrere Alternativen evaluiert, bevor wir uns für Tauri entschieden haben. Die entscheidenden Faktoren waren klar:

  • System-Webview statt gebündeltem Chromium. Tauri nutzt die vom Betriebssystem bereitgestellte Webview (WebKit unter macOS, WebView2 unter Windows, WebKitGTK unter Linux). Allein das eliminiert den Großteil von Electrons Binärgröße.
  • Rust-Backend. Das Backend läuft als kompilierte Rust-Binary. Speichersicherheit, kein Garbage Collector, vorhersehbare Performance. Für ein Tool, das Dutzende von Git-Operationen pro Interaktion ausführt, ist das entscheidend.
  • Drastisch kleinere Binärdateien. Eine Tauri-App kann unter 10 MB ausgeliefert werden. Vergleichen Sie das mit 150 MB+.
  • Plattformübergreifend mit nativer Integration. Tauri 2.x bietet solide APIs für native Menüs, System Tray, Benachrichtigungen und Dateidialoge. Die App fühlt sich an, als gehöre sie zum Betriebssystem.
  • Aktives Ökosystem und gute Entwicklererfahrung. Tauris Plugin-System, IPC-Modell und Dokumentation sind mit dem 2.x-Release erheblich gereift. Es war bereit für den produktiven Einsatz.

Die Migrationsstrategie

Ein vollständiger Rewrite ist riskant. Wir haben dieses Risiko durch eine klare Trennung der Zuständigkeiten gemindert.

Frontend behalten, Backend neu schreiben

Unser React-Frontend war bereits um eine IPC-Grenze herum strukturiert. Komponenten riefen Funktionen auf, die mit dem Electron-Hauptprozess kommunizierten; wir mussten diese Aufrufe lediglich auf Tauris invoke-API umleiten. Etwa 95 % des React-Codes konnten unverändert übernommen werden.

Das Backend war eine andere Geschichte. Jeder Node.js-Service -- Git-Operationen, Dateiüberwachung, Terminal-Verwaltung, Authentifizierung, Einstellungen -- musste in Rust neu implementiert werden. Insgesamt haben wir rund 7.200 Zeilen Rust geschrieben und über 120 IPC-Befehle migriert.

Git CLI statt libgit2

Eine häufige Frage: Warum nicht git2-rs (die Rust-Bindings für libgit2) verwenden?

Wir haben uns für std::process::Command entschieden, um die Git CLI direkt aufzurufen. Die Gründe:

  • libgit2 unterstützt nicht jedes Git-Feature. Operationen wie interaktives Rebase, einige Merge-Strategien und bestimmte Konfigurationsoptionen fehlen entweder oder verhalten sich anders.
  • Nutzer haben Git bereits installiert. Indem wir ihre lokale Git-Binary aufrufen, garantieren wir Verhaltensgleichheit mit der Kommandozeile. Wenn es im Terminal funktioniert, funktioniert es in GitSquid.
  • Debugging ist einfacher. Die Befehle, die wir ausführen, sind dieselben, die ein Nutzer tippen würde. Wenn etwas schiefgeht, sind die Fehlermeldungen vertraut.

Ersatz für das Node.js-Ökosystem

Jede Node.js-Abhängigkeit wurde durch ein Rust-Äquivalent ersetzt:

  • simple-git wurde durch direkte Git-CLI-Aufrufe via std::process::Command ersetzt
  • node-pty wurde durch portable-pty für die Terminal-Emulation ersetzt
  • Token-Speicherung wechselte zum keyring-Crate, das den OS-Schlüsselbund nutzt (macOS Keychain, Windows Credential Manager, Linux Secret Service)
  • Dateiüberwachung wechselte zum notify-Crate
  • HTTP-Anfragen wechselten zu reqwest

Nahtlose Einstellungsmigration

Wir haben die gleiche Konfigurationsverzeichnisstruktur beibehalten. Nutzer, die von v1 auf v2 upgraden, verlieren weder ihre Einstellungen noch gespeicherte Repositories oder Präferenzen. Das Rust-Backend liest dieselben Konfigurationsdateien, die das Node.js-Backend geschrieben hat.

Herausforderungen

Die Migration verlief nicht reibungslos. Mehrere Probleme kosteten uns erhebliche Debugging-Zeit.

Null-Bytes beim Git-Log-Parsing

Wir verwenden benutzerdefinierte Format-Strings mit git log --format, um Commit-Daten zu parsen. In der Electron-Version nutzten wir bestimmte Trennzeichen zwischen den Feldern. Beim Portieren nach Rust versuchten wir zunächst, Null-Bytes (\x00) als Feldtrenner zu verwenden -- sie schienen eine sichere Wahl, da sie nie in Commit-Nachrichten vorkommen würden.

Falsch gedacht. Rust-Strings verarbeiten Null-Bytes problemlos, aber mehrere Schichten des Stacks behandeln sie als C-Style-String-Terminatoren. Die Log-Ausgabe wurde am ersten Null-Byte stillschweigend abgeschnitten. Wir wechselten zu einem mehrteiligen Trennzeichen, das nie natürlich in der Git-Ausgabe vorkommt.

Keychain-Token-Speicherung

Das keyring-Crate speichert Anmeldedaten im OS-Schlüsselbund, was der richtige Ansatz ist. Aber wir stießen auf ein ähnliches Null-Byte-Problem: Wir verketteten Token-Daten mit Null-Byte-Trennzeichen, bevor wir sie speicherten. Beim Abrufen schnitten einige OS-Keychain-Implementierungen den Wert am ersten Null-Byte ab und gaben unvollständige Anmeldedaten zurück. Die Lösung war dieselbe -- ein sichtbares, eindeutiges Trennzeichen verwenden.

Komplexität der BitBucket-Authentifizierung

GitHub und GitLab haben relativ unkomplizierte tokenbasierte Auth-Flows. BitBucket ist komplexer. Je nach Operation und Kontotyp benötigt man möglicherweise einen Benutzernamen, eine E-Mail, ein App-Passwort oder ein OAuth-Token -- und die Regeln, welches wann zu verwenden ist, sind nicht immer offensichtlich. Die BitBucket-Authentifizierung zuverlässig über Clone, Fetch, Push und API-Aufrufe hinweg zum Laufen zu bringen, erforderte mehrere Iterationen.

Zeilenweises Staging

GitSquid unterstützt das Staging einzelner Zeilen aus einem Diff, nicht nur ganzer Hunks. In der Electron-Version erfolgte die Patch-Generierung in JavaScript. Für die Tauri-Version verlagerten wir diese Logik ins Rust-Backend. Valides Git-Patch-Format aus beliebigen Zeilenselektionen zu generieren, Randfälle mit Kontextzeilen zu behandeln und sicherzustellen, dass git apply das Ergebnis akzeptiert -- dies war einer der komplexeren Teile des Rewrites. Die Rust-Implementierung erwies sich letztlich als robuster als die ursprüngliche JavaScript-Version.

Französische Apostrophe, die Builds brechen

Dieser Fall war denkwürdig. Einige unserer nutzersichtbaren Strings enthielten französische typografische Apostrophe (die geschwungene Variante, U+2019) anstelle von geraden ASCII-Apostrophen. Diese verursachten Encoding-Probleme während des Build-Prozesses bei bestimmten CI-Konfigurationen. Die Lösung war trivial, sobald das Problem identifiziert war, aber die Fehlermeldungen deuteten nirgendwo in die Nähe des eigentlichen Problems.

Ergebnisse

Nach Abschluss der Migration und Erreichen der vollständigen Feature-Parität sind hier unsere Ergebnisse:

  • App-Größe: deutlich reduziert im Vergleich zum Electron-Build. Das Tauri-Bundle ist ein Bruchteil der 150 MB+, die wir zuvor ausgeliefert haben.
  • Speicherverbrauch: niedrigere Baseline und vorhersehbarer unter Last. Kein Chromium-Prozessbaum, der im Hintergrund RAM frisst.
  • Startzeit: merklich schneller. Die App ist fast sofort interaktiv.
  • Feature-Parität: 100 %. Jedes Feature aus v1 existiert in v2. Das integrierte Terminal, zeilenweises Staging, Multi-Remote-Support, Branch-Verwaltung -- alles.
  • Frontend-Wiederverwendung: 95 % der React-Codebasis konnten übernommen werden, was unsere Architekturentscheidung bestätigt, eine saubere IPC-Grenze beizubehalten.

Hat es sich gelohnt?

Ja.

Der Rewrite erforderte erheblichen Aufwand. Rund 7.200 Zeilen Rust, 120+ IPC-Befehle zum Neu-Implementieren und mehrere nicht offensichtliche Bugs, die nur auftraten, weil Rust und Node.js Randfälle unterschiedlich behandeln. Es war kein Wochenendprojekt.

Aber das Ergebnis ist eine grundlegend bessere Anwendung. GitSquid v2 startet schneller, verbraucht weniger Speicher, wird als kleinere Binary ausgeliefert und fühlt sich auf jeder Plattform nativer an. Das Rust-Backend ist vorhersehbarer und leichter nachvollziehbar als das Node.js-Äquivalent. Und da wir dasselbe React-Frontend beibehalten haben, sehen die Nutzer die gleiche Oberfläche, die sie bereits kennen.

Wenn Sie eine Electron-App pflegen und der Runtime-Overhead Sie stört, ist Tauri eine ernsthafte Option. Das Ökosystem ist reif genug für den produktiven Einsatz, und der Migrationspfad -- besonders wenn Sie bereits eine saubere Frontend/Backend-Trennung haben -- ist handhabbarer, als ein vollständiger Rewrite vermuten lässt.

Die zentrale Erkenntnis: Behandeln Sie Ihre IPC-Grenze als API-Vertrag. Wenn Ihr Frontend nur weiß, wie man invoke('get_commits', { path, branch }) aufruft, und es egal ist, ob die Antwort von Node.js oder Rust kommt, wird der Backend-Austausch zu einem realistischen Projekt statt einer Fantasie.