Skip to content

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

Anurag Verma

7 min read

Tauri 2.0: Build Desktop and Mobile Apps with Web Tech, Without the Electron Bloat

Sponsored

Share

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

FactorTauri 2.0Electron
Installer size~600KB-5MB100-200MB
Rendering engineOS webview (varies)Chromium (consistent)
Backend languageRustNode.js
Mobile supportiOS + Android (v2)None
Memory usageLowerHigher
Debugging toolsSafari/WebView2 inspectorChrome DevTools
Plugin ecosystemGrowingMature

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

Enjoyed it? Pass it on.

Share this article.

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.

No spam, ever. Unsubscribe anytime.

Discussion

Join the conversation.

Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.

Sponsored