G.STANCUTA
Veröffentlicht · 2026 · 05 · 128 Min. Lesezeit

Desktop-Apps ohne die Chromium-Steuer: Rust, Tauri und Polysong

  • rust
  • tauri
  • desktop
  • sqlite

Electron liefert mit jeder App einen vollständigen Browser aus. Tauri nutzt die OS-Webview und einen Rust-Kern, wodurch die Bundle-Größe um das 10-fache und der Arbeitsspeicher um die Hälfte reduziert werden. Das ist der tatsächliche Preis dieses Kompromisses.

Ich wollte einen lokal-orientierten Musikbibliotheks-Manager. Etwas, das eine Verzeichnisstruktur scannt, Audio-Tags liest, alles in SQLite speichert und nie das Netzwerk berührt. Polysong ist diese App. Ich habe sie mit Rust und Tauri gebaut und nach sechs Monaten täglicher Nutzung habe ich ein klares Bild davon, wo dieser Stack seinen Ruf verdient und wo er einem still ein Problem übergibt.

Die Kernarchitektur

Tauri teilt die App in zwei Prozesse auf. Das Backend ist ein Rust-Binary, das mit der Geschäftslogik kompiliert wird. Das Frontend ist ein beliebiger Web-Stack, der innerhalb der nativen OS-Webview gerendert wird: WebKit auf macOS, WebView2 (Chromium-basiert) auf Windows und WebKitGTK auf Linux. Diese beiden Seiten kommunizieren über eine typisierte Befehls-Bridge.

Die Webview wird nicht mit der App ausgeliefert. Der Nutzer hat sie bereits. Auf macOS und modernem Windows kommt sie mit dem Betriebssystem. Genau dieser Umstand erklärt, warum ein Tauri-App-Installer 4-8 MB wiegen kann, während eine vergleichbare Electron-App 80-150 MB wiegt. Das Rust-Binary wird vorab kompiliert, sodass auch keine Node.js-Laufzeit benötigt wird.

Tauri-Architekturdiagramm mit dem Rust-Kern, der OS-Webview und der Befehls-Bridge
Tauri trennt die Arbeit sauber: Rust übernimmt I/O und Daten, die OS-Webview rendert die Benutzeroberfläche.

Die Befehls-Bridge

Jeder Aufruf vom Frontend zu Rust läuft über eine #[tauri::command]-Funktion. Argumente werden aus JSON deserialisiert, die Funktion wird ausgeführt und der Rückgabewert wird wieder serialisiert. Die Typen sind auf beiden Seiten explizit. Wenn ein Feld falsch benannt oder die falsche Form übergeben wird, erhält man einen Laufzeitfehler in der Rust-Schicht statt stiller Datenverfälschung.

Hier ist der Kernbefehl, den Polysong verwendet, um die Bibliothek abzufragen. Er nimmt einen Suchstring und einen Sortierschlüssel entgegen, greift über rusqlite auf SQLite zu und gibt einen Vektor von Track-Datensätzen zurück.

rust
use serde::{Deserialize, Serialize};
use tauri::State;
use rusqlite::params;

use crate::db::DbConn;

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TrackRecord {
    pub id: i64,
    pub title: String,
    pub artist: String,
    pub album: String,
    pub duration_secs: f64,
    pub file_path: String,
}

#[tauri::command]
pub async fn search_tracks(
    query: String,
    sort_by: Option<String>,
    db: State<'_, DbConn>,
) -> Result<Vec<TrackRecord>, String> {
    let conn = db.lock().map_err(|e| e.to_string())?;
    let sort_col = match sort_by.as_deref() {
        Some("artist") => "artist, title",
        Some("album")  => "album, track_num",
        _              => "title",
    };
    let pattern = format!("%{}%", query.to_lowercase());
    let sql = format!(
        "SELECT id, title, artist, album, duration_secs, file_path
         FROM tracks
         WHERE lower(title) LIKE ?1
            OR lower(artist) LIKE ?1
            OR lower(album) LIKE ?1
         ORDER BY {sort_col}
         LIMIT 500"
    );
    let mut stmt = conn.prepare(&sql).map_err(|e| e.to_string())?;
    let rows = stmt
        .query_map(params![pattern], |row| {
            Ok(TrackRecord {
                id:            row.get(0)?,
                title:         row.get(1)?,
                artist:        row.get(2)?,
                album:         row.get(3)?,
                duration_secs: row.get(4)?,
                file_path:     row.get(5)?,
            })
        })
        .map_err(|e| e.to_string())?;
    rows.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
}

Auf der Frontend-Seite ist der Aufruf ein einzelnes invoke aus @tauri-apps/api/core. TypeScript-Typen werden automatisch generiert, wenn das Flag plugin generate bindings der Tauri CLI verwendet wird, oder man kann sie für kleine Oberflächen von Hand schreiben.

ts
import { invoke } from '@tauri-apps/api/core';

interface TrackRecord {
  id: number;
  title: string;
  artist: string;
  album: string;
  duration_secs: number;
  file_path: string;
}

export async function searchTracks(
  query: string,
  sortBy?: 'title' | 'artist' | 'album',
): Promise<TrackRecord[]> {
  return invoke<TrackRecord[]>('search_tracks', {
    query,
    sort_by: sortBy ?? null,
  });
}

Konfiguration: tauri.conf.json

Das meiste Tauri-Verhalten wird über tauri.conf.json gesteuert. Für Polysong schränke ich die Capability-Oberfläche aggressiv ein: kein Shell-Zugriff aus der Webview, Dateisystemzugriff auf das Musikbibliotheksverzeichnis beschränkt, kein beliebiges HTTP vom Frontend. Dies ist eines der stärksten Argumente für Tauri gegenüber Electron aus Sicherheitsperspektive. Die Standardhaltung ist Deny-all; Capabilities werden explizit hinzugefügt.

json
{
  "productName": "Polysong",
  "version": "0.3.0",
  "identifier": "io.polysong.app",
  "build": {
    "frontendDist": "../dist",
    "devUrl": "http://localhost:5173"
  },
  "app": {
    "windows": [
      {
        "title": "Polysong",
        "width": 1200,
        "height": 780,
        "minWidth": 800,
        "minHeight": 560,
        "decorations": true,
        "resizable": true
      }
    ],
    "security": {
      "csp": "default-src 'self'; img-src 'self' asset: https://asset.localhost"
    }
  },
  "bundle": {
    "active": true,
    "targets": "all",
    "icon": ["icons/32x32.png", "icons/128x128.png", "icons/icon.icns", "icons/icon.ico"]
  }
}

Der echte Kompromiss: Webview-Divergenz zwischen Plattformen

Hier verdient Tauri sein Sternchen. Auf macOS und Linux erhält man WebKit. Auf Windows erhält man WebView2, das Chromium-basiert ist. Es handelt sich nicht um dieselbe Engine, und die Unterschiede zeigen sich im CSS- und JavaScript-Verhalten, insbesondere bei Audio, Video und einigen Layout-Randfällen.

Bei Polysong hat mich das in der Audio-Pipeline erwischt. Die Web Audio API verhält sich zwischen WebKit und WebView2 bei bestimmten Puffergrößen leicht unterschiedlich. Am Ende habe ich die Audio-Wiedergabe vollständig in die Rust-Schicht verschoben, mit rodio, und eine kleine Befehlsoberfläche für Play, Pause, Seek und Lautstärke bereitgestellt. Das war ohnehin vermutlich die richtige Entscheidung, da Rust mir lückenlose Wiedergabe und ReplayGain ohne zusätzliche Verrenkungen bietet.

  • Alle drei Plattformen testen, bevor man sich auf CSS-Animationen oder Web-Audio-Code festlegt
  • Alles, was leistungssensibel oder plattformspezifisch ist, gehört in Rust, nicht in die Webview
  • CSS Grid und Flexbox sind auf allen drei Plattformen konsistent genug, um einmal geschrieben zu werden
  • Schriften rendern zwischen WebKit und WebView2 unterschiedlich; font-synthesis: none verwenden und eigene Schriften ausliefern
  • WebView2 unter Windows erfordert die Installation der WebView2-Laufzeit; modernes Windows 11 liefert sie mit, ältere Systeme möglicherweise nicht

Der Arbeitsspeicherbedarf ist tatsächlich besser als bei Electron. Ein frisches Polysong-Fenster mit einer geladenen Bibliothek von 40.000 Titeln liegt bei etwa 90 MB RSS. Die vergleichbare Electron-App, die ich früher prototypisiert hatte, erreichte 250 MB bei derselben Arbeit. Der größte Teil des Tauri-Footprints ist der Webview-Renderer selbst; das Rust-Binary ist vernachlässigbar.

Diesen Stack mit einem KI-Coding-Agenten verwenden

Ich arbeite regelmäßig mit einem KI-Coding-Agenten (Claude Code) an Polysong. Rust plus Tauri plus SQLite ist keine kleine Oberfläche. Der Agent hat solides allgemeines Rust-Wissen, aber Tauri 2.x hat Breaking Changes gegenüber 1.x, das Capability-System ist neu und meine Projektkonventionen sind spezifisch. Ohne dauerhaften Kontext macht der Agent Rückschritte: Er schreibt die allowlist-Syntax von Tauri 1.x, vergisst, dass Audio in Rust lebt, schlägt falsche Crate-Versionen vor.

Die Lösung ist eine AGENTS.md-Datei im Projektstammverzeichnis. Sie ist in git eingecheckt, immer vorhanden und ich aktualisiere sie jedes Mal, wenn ich eine strukturelle Entscheidung treffe. Der Agent liest sie zu Beginn jeder Sitzung und arbeitet mit dem vollständigen aktuellen Bild des Projekts.

Diagramm eines KI-Agenten, der Markdown-Gedächtnisdateien liest, um den Projektkontext über Sitzungen hinweg beizubehalten
Persistente Markdown-Kontextdateien geben dem Agenten ein Langzeitgedächtnis für die Konventionen des Projekts.

Hier die tatsächliche AGENTS.md von Polysong, leicht gekürzt:

md
# Polysong – Agent Context

## Stack
- Rust (stable, 1.78+) + Tauri 2.x + SQLite via rusqlite 0.31
- Frontend: SvelteKit (static adapter, no SSR), TypeScript, Tailwind 4
- Audio playback: Rust only, via rodio 0.18. Do NOT use Web Audio API for playback.
- DB migrations: barrel-migrations crate, files in src-tauri/migrations/

## Tauri version note
This project uses **Tauri 2.x**. The old allowlist is GONE.
Permissions live in src-tauri/capabilities/. Each capability file is JSON.
Never write tauri::Builder allowlist config — it will not compile.

## Commands
- `cargo tauri dev` – dev mode with hot reload
- `cargo tauri build` – production bundle
- `cargo test` – Rust unit tests (run from src-tauri/)
- `pnpm dev` – frontend only (not connected to Tauri backend)

## Architecture decisions
- All file system access goes through Rust commands. The frontend never touches the FS directly.
- Audio: playback, seek, and volume are Rust commands. The frontend sends intents, gets state back.
- Search is SQLite FTS5 for albums/artists, LIKE for track titles (FTS5 overkill for title search).
- Window state (last size/position) is saved via tauri-plugin-window-state.

## Known gotchas
- WebKit (macOS/Linux) and WebView2 (Windows) differ on Web Audio. Keep audio in Rust.
- rusqlite must be compiled with the `bundled` feature on Windows or linking fails.
- Font rendering differs between platforms. Use `font-synthesis: none` and local fonts.
- The migrations barrel runner requires migration files numbered 001_, 002_ etc., no gaps.

## File layout
```
src-tauri/
  src/
    commands/   # Tauri command handlers
    db/         # Connection pool and query helpers
    audio/      # rodio playback engine
  migrations/   # SQL migration files
  capabilities/ # Tauri 2 permission manifests
src/            # SvelteKit frontend
```

Diese Datei hat die meisten Kontestverlust-Regressionen beseitigt. Wenn ich eine neue Agentensitzung öffne, sage ich "lies zuerst AGENTS.md" und der Agent arbeitet sofort im richtigen Rahmen: korrekte Tauri-Version, korrekte Crate-Versionen, korrekte Architektur-Einschränkungen. Er hört auf, Web Audio für die Wiedergabe vorzuschlagen. Er schreibt Capability-Manifests statt Allowlists. Die Qualität des ersten Versuchs bei jeder Aufgabe steigt deutlich.

Lohnt sich Tauri

Für Polysong: ja, eindeutig. Das Bundle ist klein, der Arbeitsspeicher ist gering und I/O-intensive Arbeit in Rust auszulagern ist angenehm statt schmerzhaft. Die SQLite-Integration über rusqlite ist die beste, die ich je in einer Sprache verwendet habe. Lückenlose Audio-Wiedergabe in rodio hat einen Nachmittag gedauert.

Die Webview-Divergenz zwischen Plattformen ist real, aber beherrschbar, wenn man einer Regel folgt: Alles, was OS-Primitive, Audio, Video, Datei-I/O oder leistungsempfindliche Berechnungen berührt, gehört in Rust. Die Webview ist eine Rendering-Oberfläche. Bleibt man dabei, hält die plattformübergreifende Geschichte stand.

Wenn man umfangreiche Web-Inhalte benötigt, die sich überall wirklich identisch verhalten müssen, ist Electron noch immer die pragmatische Wahl. Aber wenn eine Dienstprogramm-App mit echter Systemintegration gebaut wird, liefert Tauri ein kleineres, schnelleres und besser verteidigbares Ergebnis. Die Rust-Lernkurve ist der ehrliche Preis. Sie ist nicht flach, aber sie zahlt sich in einer Codebasis aus, in der Korrektheit wirklich wichtig ist.

Tauri macht Desktop-Entwicklung nicht einfacher. Es macht sie kleiner, schneller und ehrlicher darüber, wo die Arbeit hingehört.

Portfolio · Schriftfeld
Gezeichnet von
G. STANCUTA
Disziplin
AI & AUTOMATION
Standort
MORTER · SÜDTIROL
Status
Verfügbar
Sprachen
IT · EN · RO · DE+
Stack
PLOI · HETZNER
Revision
REV 2026.A
2026

© 2026 Gabriel Stancuta · jumpinotech.com — Mit KI entworfen, gebaut, um sich selbst zu betreiben.