Web Development · Desktop Development
Tauri 2.0: Build Desktop and Mobile Apps with Web Tech, Without the Electron Bloat
Tauri 2.0 added iOS and Android support while keeping what made 1.x compelling: tiny binaries, Rust backend, and native OS webviews instead of a bundled Chromium. Here's how it works and when to use it.
Anurag Verma
7 min read
Sponsored
Electron ships a full copy of Chromium with every app. A minimal Electron app with a simple HTML page weighs around 100-150MB as an installer. This is the tradeoff the team accepts to get a consistent rendering engine and Chrome DevTools as a debugger.
Tauri takes a different approach: use the OS’s built-in webview instead of bundling one. On macOS, it uses WebKit. On Windows, it uses WebView2 (the Chromium-based webview that ships with Windows 10 and 11). On Linux, it uses WebKitGTK. The app’s frontend code is standard HTML/CSS/JavaScript (React, Vue, Svelte, or plain HTML). The backend logic runs in Rust, handling anything that needs native access: file system, database connections, system APIs, notifications.
Tauri 2.0, released in October 2024, added mobile targets to the mix. The same Tauri app can now compile for iOS and Android alongside desktop platforms, sharing the Rust business logic.
App Size in Practice
The size difference over Electron is the most cited number, so let’s be specific about what it means.
A minimal Tauri app with a static HTML page produces an installer of around 600KB on macOS. Add a full React frontend with several dependencies, and it’s still typically under 5MB for the installer. The installed footprint is a few MB, not the 300-500MB range Electron apps often reach.
Why? The Chromium binary embedded in Electron is 60-80MB on its own. Tauri’s binary includes only the Rust runtime and your app logic. The webview rendering engine is already on the user’s machine as part of the OS.
The tradeoff: you lose rendering consistency. Safari’s WebKit renders some things differently from Chromium. If your frontend relies on features not supported in all three webviews (WebKit on macOS/iOS, WebView2 on Windows, WebKitGTK on Linux), you’ll hit inconsistencies. Figma, for example, couldn’t use Tauri for this reason. Their canvas rendering had too many WebKit-specific differences. For most apps using standard web APIs, the inconsistencies are minor and manageable.
Project Structure
Creating a new Tauri project requires Rust (via rustup) and Node.js:
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Create a new Tauri app with React frontend
npm create tauri-app@latest my-app
cd my-app
npm install
npm run tauri dev
The project has two parts:
my-app/
├── src/ # Frontend (React, Vue, Svelte, or plain HTML/JS)
│ ├── App.tsx
│ └── main.tsx
├── src-tauri/ # Rust backend
│ ├── src/
│ │ ├── lib.rs # Tauri commands and app setup
│ │ └── main.rs # Entry point
│ ├── Cargo.toml # Rust dependencies
│ └── tauri.conf.json # App configuration
└── package.json
The frontend is built with whatever bundler you choose (Vite is the default). Tauri serves the built frontend from a local server during development, and bundles it into the app binary for release.
Calling Rust from JavaScript
Communication between the frontend and the Rust backend goes through commands, which are functions you define in Rust and invoke from JavaScript.
Define a command in src-tauri/src/lib.rs:
use tauri::command;
#[command]
fn read_config_file(path: String) -> Result<String, String> {
std::fs::read_to_string(&path)
.map_err(|e| e.to_string())
}
#[command]
async fn fetch_from_db(query: String) -> Result<Vec<String>, String> {
// async commands work too
// use any Rust library here: sqlx, reqwest, etc.
Ok(vec!["result1".to_string(), "result2".to_string()])
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
read_config_file,
fetch_from_db,
])
.run(tauri::generate_context!())
.expect("error while running Tauri application");
}
Call it from the frontend:
import { invoke } from "@tauri-apps/api/core";
async function loadConfig(filePath: string) {
try {
const content = await invoke<string>("read_config_file", {
path: filePath,
});
console.log("Config:", content);
} catch (error) {
console.error("Failed to read config:", error);
}
}
The invoke function is type-safe with TypeScript generics. The argument names must match the Rust function’s parameter names.
Listening to Events
For updates that flow from Rust to the frontend without a request (progress updates, background task results, real-time data), Tauri uses events:
use tauri::{AppHandle, Emitter};
#[command]
async fn start_download(url: String, app: AppHandle) -> Result<(), String> {
// Simulate a download with progress updates
for progress in 0..=100 {
app.emit("download-progress", progress)
.map_err(|e| e.to_string())?;
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
Ok(())
}
Listen in the frontend:
import { listen } from "@tauri-apps/api/event";
const unlisten = await listen<number>("download-progress", (event) => {
console.log(`Progress: ${event.payload}%`);
setProgress(event.payload);
});
// Clean up when component unmounts
onCleanup(() => unlisten());
The Permission System
Tauri 2.0 uses a capability-based permission model. The frontend can only call commands and access native APIs that you explicitly grant it in tauri.conf.json and capability files:
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"fs:read-all",
"shell:open",
"dialog:open"
]
}
This model means a compromised frontend (say, from a malicious website loaded in a webview) can only do what you’ve explicitly permitted. Electron’s default model gave the renderer process Node.js access, which led to a class of vulnerabilities where XSS in an Electron app could read files off the user’s disk. Tauri’s permission system makes this much harder.
Mobile Targets
Tauri 2.0 added android and ios as build targets, sharing the Rust backend logic across platforms.
Add a mobile target:
# iOS (requires macOS and Xcode)
npm run tauri ios init
npm run tauri ios dev
# Android (requires Android Studio and NDK)
npm run tauri android init
npm run tauri android dev
The frontend HTML/CSS/JS renders in the native webview (WKWebView on iOS, WebView on Android). The Rust backend handles native functionality via Tauri plugins. The same invoke pattern works on mobile.
Mobile plugins cover the expected capabilities: camera, notifications, geolocation, biometric auth, haptics, sharing. Most require additional permissions in the platform manifests.
The mobile support in 2.0 is functional but younger than the desktop support. For straightforward apps (forms, content readers, productivity tools), it works. For apps that push native platform capabilities heavily, React Native or Flutter still have more mature ecosystems.
When Tauri Fits
Internal tools and desktop utilities: File processors, database browsers, log viewers, API clients, configuration managers. These are apps where tiny size and native OS feel matter, but a SaaS web app would work just as well. Tauri apps can be distributed via a GitHub releases page rather than a store, keeping distribution simple.
Apps that need both desktop and web versions: Your frontend is already HTML/CSS/JS. Wrapping it in Tauri for distribution as a desktop app adds minimal complexity compared to maintaining a separate native app.
Teams with Rust capability: If your team already writes Rust, the backend layer is natural. If Rust is unfamiliar, the learning curve for backend logic is real, though the community has pre-built plugins for most common needs.
Privacy-sensitive tools: The permission model and no-browser-process isolation are meaningful for apps handling credentials, files, or sensitive data.
When Tauri Doesn’t Fit
WebGL-heavy or graphics-intensive apps: WebKit and WebView2 have different WebGL support levels. Apps doing complex 3D rendering or pixel-level canvas manipulation may hit rendering differences that are painful to work around.
Large teams that need Chrome DevTools compatibility for debugging: Electron’s guaranteed Chromium means Chrome DevTools works identically. Tauri’s WebKit on macOS means using Safari’s Web Inspector for debugging, which behaves differently.
Apps distributed through stores where size isn’t a concern: If your app sells on the Mac App Store or Microsoft Store and bundle size doesn’t matter, Electron’s developer experience advantages might outweigh Tauri’s size benefits.
Comparison with Electron
| Factor | Tauri 2.0 | Electron |
|---|---|---|
| Installer size | ~600KB-5MB | 100-200MB |
| Rendering engine | OS webview (varies) | Chromium (consistent) |
| Backend language | Rust | Node.js |
| Mobile support | iOS + Android (v2) | None |
| Memory usage | Lower | Higher |
| Debugging tools | Safari/WebView2 inspector | Chrome DevTools |
| Plugin ecosystem | Growing | Mature |
The tradeoff is reproducible: consistent rendering at a large size (Electron), or small size at the cost of rendering consistency (Tauri). For most internal tools and utilities where rendering edge cases are controllable, Tauri is the better default choice in 2026. For consumer apps where pixel-perfect cross-platform consistency matters, Electron’s tradeoffs remain justified.
Sponsored
More from this category
More from Web Development
CSS Anchor Positioning: Tooltips and Popovers Without JavaScript
gRPC in 2026: When to Use It Instead of REST or GraphQL
k6 Load Testing: Performance Testing Your APIs Before Users Find the Problems
Sponsored
The dispatch
Working notes from
the studio.
A short letter twice a month — what we shipped, what broke, and the AI tools earning their keep.
Discussion
Join the conversation.
Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.
Sponsored