App Desktop Senza il Peso di Chromium: Rust, Tauri e Polysong
- rust
- tauri
- desktop
- sqlite
Electron include un browser completo in ogni app. Tauri usa la webview del sistema operativo e un core in Rust, riducendo la dimensione del pacchetto di 10 volte e la memoria della metà. Ecco cosa comporta davvero quella scelta.
Volevo un gestore locale di librerie musicali. Qualcosa che scansioni una struttura di cartelle, legga i tag audio, salvi tutto in SQLite e non tocchi mai la rete. Polysong è quella app. L'ho costruita con Rust e Tauri e dopo sei mesi di uso quotidiano ho un quadro chiaro di dove questo stack si guadagna la sua reputazione e dove ti passa silenziosamente un problema.
L'Architettura di Base
Tauri divide l'app in due processi. Il backend è un binario Rust compilato con la tua logica di business. Il frontend è qualsiasi stack web tu voglia, renderizzato all'interno della webview nativa del sistema operativo: WebKit su macOS, WebView2 (basato su Chromium) su Windows e WebKitGTK su Linux. I due lati comunicano attraverso un ponte di comandi tipizzato.
La webview non viene distribuita con la tua app. L'utente la possiede già. Su macOS e Windows moderni è inclusa nel sistema operativo. Questo unico fatto spiega perché un installer di Tauri può pesare 4-8 MB mentre un'app Electron equivalente pesa 80-150 MB. Il binario Rust è compilato anticipatamente, quindi non c'è nemmeno un runtime Node.js.

Il Ponte di Comandi
Ogni chiamata dal frontend a Rust passa attraverso una funzione #[tauri::command]. Gli argomenti vengono deserializzati da JSON, la funzione viene eseguita e il valore restituito viene serializzato di nuovo. I tipi sono espliciti su entrambi i lati. Se si sbaglia il nome di un campo o si passa la forma sbagliata, si ottiene un errore a runtime nel layer Rust piuttosto che una corruzione silenziosa dei dati.
Ecco il comando principale che Polysong usa per interrogare la libreria. Prende una stringa di ricerca e una chiave di ordinamento, accede a SQLite tramite rusqlite e restituisce un vettore di record di tracce.
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())
}Lato frontend, la chiamata è un singolo invoke da @tauri-apps/api/core. I tipi TypeScript vengono generati automaticamente se si usa il flag plugin generate bindings della CLI di Tauri, oppure si possono scrivere a mano per superfici ridotte.
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,
});
}Configurazione: tauri.conf.json
La maggior parte del comportamento di Tauri è controllata da tauri.conf.json. Per Polysong blocco la superficie delle capability in modo aggressivo: nessun accesso alla shell dalla webview, accesso al filesystem limitato alla directory della libreria musicale, nessun HTTP arbitrario dal frontend. Questo è uno degli argomenti migliori a favore di Tauri rispetto a Electron dal punto di vista della sicurezza. La postura predefinita è deny-all: le capability vengono aggiunte esplicitamente.
{
"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"]
}
}Il Vero Compromesso: Divergenza della Webview tra Piattaforme
È qui che Tauri guadagna il suo asterisco. Su macOS e Linux si ottiene WebKit. Su Windows si ottiene WebView2, basato su Chromium. Non sono lo stesso motore e le differenze emergono nel comportamento di CSS e JavaScript, specialmente per audio, video e alcuni casi limite di layout.
Per Polysong il problema si è presentato nella pipeline audio. La Web Audio API si comporta in modo leggermente diverso tra WebKit e WebView2 per certe dimensioni di buffer. Alla fine ho spostato la riproduzione audio interamente nel layer Rust usando rodio ed esposto una piccola superficie di comandi per play, pause, seek e volume. Era comunque probabilmente la scelta giusta, visto che Rust mi offre la riproduzione gapless e il ReplayGain senza acrobazie extra.
- Testa su tutte e tre le piattaforme prima di impegnarsi su qualsiasi animazione CSS o codice Web Audio
- Tutto ciò che è sensibile alle prestazioni o specifico della piattaforma appartiene a Rust, non alla webview
- CSS grid e flexbox sono sufficientemente coerenti su tutte e tre le piattaforme da poter essere scritti una sola volta
- I font vengono renderizzati diversamente tra WebKit e WebView2; usa
font-synthesis: nonee servi i tuoi font - WebView2 su Windows richiede che il runtime WebView2 sia installato; Windows 11 moderno lo include, i sistemi più vecchi potrebbero non averlo
Il consumo di memoria è genuinamente migliore rispetto a Electron. Una finestra Polysong appena aperta con una libreria di 40.000 tracce caricate si assesta intorno a 90 MB RSS. L'app Electron equivalente che avevo prototipato in precedenza raggiungeva i 250 MB facendo lo stesso lavoro. La maggior parte del footprint di Tauri è il renderer della webview stessa; il binario Rust è trascurabile.
Usare Questo Stack con un Agente AI per il Codice
Lavoro regolarmente con un agente AI per il codice (Claude Code) su Polysong. Rust più Tauri più SQLite non è una superficie piccola. L'agente ha una solida conoscenza generale di Rust, ma Tauri 2.x presenta breaking change rispetto alla versione 1.x, il sistema di capability è nuovo e le convenzioni del mio progetto sono specifiche. Senza contesto persistente l'agente regredisce: scrive la sintassi allowlist di Tauri 1.x, dimentica che l'audio vive in Rust, suggerisce versioni di crate sbagliate.
La soluzione è un file AGENTS.md nella radice del progetto. È tracciato in git, sempre presente e lo aggiorno ogni volta che prendo una decisione strutturale. L'agente lo legge all'inizio di ogni sessione e opera con il quadro attuale completo del progetto.

Ecco il vero AGENTS.md di Polysong, leggermente abbreviato:
# 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
```Quel file ha eliminato la maggior parte delle regressioni da perdita di contesto. Quando apro una nuova sessione con l'agente, dico "leggi prima AGENTS.md" e l'agente opera immediatamente nel frame corretto: versione Tauri corretta, versioni di crate corrette, vincoli architetturali corretti. Smette di suggerire Web Audio per la riproduzione. Scrive manifest di capability invece di allowlist. La qualità del primo tentativo su qualsiasi compito migliora significativamente.
Tauri Vale la Pena
Per Polysong: sì, chiaramente. Il pacchetto è piccolo, la memoria è bassa e spostare il lavoro pesante di I/O in Rust è piacevole piuttosto che doloroso. L'integrazione SQLite tramite rusqlite è la migliore che abbia mai usato in qualsiasi linguaggio. La riproduzione audio gapless in rodio ha richiesto un pomeriggio.
La divergenza della webview tra piattaforme è reale ma gestibile se si segue una regola: tutto ciò che tocca le primitive del sistema operativo, audio, video, I/O di file o calcolo sensibile alle prestazioni va in Rust. La webview è una superficie di rendering. Mantienila tale e la storia multipiattaforma regge.
Se hai bisogno di contenuti web ricchi che devono davvero comportarsi in modo identico ovunque, Electron è ancora la scelta pragmatica. Ma se stai costruendo un'app di utilità con vera integrazione di sistema, Tauri ti dà un risultato più piccolo, più veloce e più difendibile. La curva di apprendimento di Rust è il costo onesto. Non è superficiale, ma si ripaga in una codebase dove la correttezza conta davvero.
Tauri non rende lo sviluppo desktop più facile. Lo rende più piccolo, più veloce e più onesto su dove appartiene il lavoro.