Skip to main content
  1. Languages/
  2. Rust Guides/

Mastering Desktop Development: Building Lightweight Apps with Tauri and Rust

Jeff Taakey
Author
Jeff Taakey
21+ Year CTO & Multi-Cloud Architect.

For years, the desktop application landscape was dominated by a single narrative: if you wanted cross-platform compatibility and a modern UI, you had to accept the heavy resource tax of Electron. We’ve all seen the memes about chat applications eating up gigabytes of RAM.

However, as we look back at the technological shifts of 2025, the narrative has changed drastically. Tauri has matured from an exciting experiment into a production-ready powerhouse. It allows developers to use existing frontend technologies (React, Vue, Svelte) while leveraging the raw power, safety, and minuscule footprint of Rust for the backend logic.

In this guide, we aren’t just building a “Hello World.” We are going to architect a functional system utility that demonstrates real-world patterns: bi-directional communication, state management, and error handling. By the end of this article, you will understand why senior developers are increasingly choosing the Tauri stack for their next generation of tools.


Prerequisites and Environment Setup
#

Before we write a single line of code, let’s ensure your environment is ready. Tauri relies on the host OS’s native web renderer (WebView2 on Windows, WebKit on macOS/Linux), which is part of why the binaries are so small.

1. System Requirements
#

  • Operating System: Windows 10/11, macOS, or a modern Linux distro (Ubuntu 22.04+).
  • Rust: Ensure you are running the stable release.
    rustc --version
    # Should be 1.75 or higher
  • Node.js: We need this for the frontend bundler.
    node --version
    # v20.0.0 or higher recommended

2. Installing the Tauri CLI
#

While you can use npx, installing the CLI globally via Cargo is often faster for Rust-centric workflows.

cargo install tauri-cli

3. IDE Configuration
#

We highly recommend VS Code with the following extensions:

  • rust-analyzer: The official Rust language server.
  • Tauri: Provides schema validation for tauri.conf.json.
  • ESLint/Prettier: For the frontend TypeScript code.

Understanding the Architecture
#

Before coding, it is crucial to understand how Tauri differs from the “Chromium-in-a-box” model (Electron). Tauri uses an Inter-Process Communication (IPC) bridge to connect your webview (frontend) with the Core process (Rust).

This separation ensures that heavy computation happens in Rust, keeping the UI thread buttery smooth.

flowchart TD subgraph Frontend ["Frontend (WebView)"] direction TB UI[React/HTML/CSS] JS[TypeScript Logic] Invoke[Tauri Invoke API] end subgraph IPC ["IPC Bridge"] JSON[Serialized JSON Messages] end subgraph Backend ["Backend (Rust Core)"] Handler[Command Handler] State[Managed State / Mutex] OS[OS / System API] end UI --> JS JS --> Invoke Invoke <-->|Asynchronous| JSON JSON <--> Handler Handler --> State Handler --> OS OS --> Handler

Step 1: Scaffolding the Project
#

We will use the official scaffolding tool. In your terminal, navigate to your workspace and run:

npm create tauri-app@latest

Follow the interactive prompts with these choices to match our tutorial:

  1. Project name: rust-sys-monitor
  2. Identifier: com.rustdevpro.sysmonitor
  3. Frontend flavor: TypeScript / React
  4. Package Manager: pnpm (or npm if you prefer)

Once created, navigate into the folder and install dependencies:

cd rust-sys-monitor
pnpm install

Step 2: The Rust Backend (The “Heavy Lifter”)
#

We are going to build a feature that Electron struggles with: low-level system monitoring with minimal overhead. We’ll use the sysinfo crate.

2.1 Dependencies
#

Open src-tauri/Cargo.toml and add the sysinfo crate and serde features.

[package]
name = "rust-sys-monitor"
version = "0.0.0"
edition = "2021"

[build-dependencies]
tauri-build = { version = "2.0", features = [] }

[dependencies]
tauri = { version = "2.0", features = [] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
# Add this line:
sysinfo = "0.30"

2.2 Implementing the Logic
#

Open src-tauri/src/main.rs. We will define a command called get_memory_usage.

Notice how we use #[tauri::command]. This macro handles the complex serialization of data between Rust types and JavaScript JSON automatically.

// src-tauri/src/main.rs

#
![cfg_attr(not(debug_assertions)
, windows_subsystem = "windows")]

use sysinfo::{System, SystemExt};
use std::sync::Mutex;
use tauri::State;

// 1. Define a struct for our return data.
// It must derive Serialize so it can be sent to the frontend.
#[derive(serde::Serialize)]
struct SystemStats {
    total_memory: u64,
    used_memory: u64,
    free_memory: u64,
    usage_percentage: f64,
}

// 2. Define a wrapper for our System state.
// Accessing system info can be expensive to initialize, so we keep it in state.
struct SysState {
    system: Mutex<System>,
}

// 3. The Command
// We borrow the state, lock the mutex, refresh specific data, and return a struct.
#[tauri::command]
fn get_memory_usage(state: State<SysState>) -> SystemStats {
    let mut sys = state.system.lock().unwrap();
    
    // Only refresh memory to save resources
    sys.refresh_memory();

    let total = sys.total_memory();
    let used = sys.used_memory();
    let free = sys.free_memory();
    
    // Avoid division by zero
    let percentage = if total > 0 {
        (used as f64 / total as f64) * 100.0
    } else {
        0.0
    };

    SystemStats {
        total_memory: total,
        used_memory: used,
        free_memory: free,
        usage_percentage: (percentage * 100.0).round() / 100.0,
    }
}

fn main() {
    tauri::Builder::default()
        // 4. Manage State
        // We initialize the System object once.
        .manage(SysState {
            system: Mutex::new(System::new_all()),
        })
        // 5. Register the command
        .invoke_handler(tauri::generate_handler
![get_memory_usage])

        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Step 3: The Frontend (The Visual Layer)
#

Now we move to the React side. We need to invoke the Rust function. Tauri provides a typed invoke function.

3.1 The React Component
#

Open src/App.tsx. We will create a simple dashboard.

// src/App.tsx
import { useState, useEffect } from "react";
import { invoke } from "@tauri-apps/api/core";
import "./App.css";

// Define the shape of data coming from Rust
// This should match the struct in main.rs
interface SystemStats {
  total_memory: number;
  used_memory: number;
  free_memory: number;
  usage_percentage: number;
}

function App() {
  const [stats, setStats] = useState<SystemStats | null>(null);

  // Function to call the Rust backend
  async function fetchStats() {
    try {
      // 'get_memory_usage' matches the function name in main.rs
      const result = await invoke<SystemStats>("get_memory_usage");
      setStats(result);
    } catch (error) {
      console.error("Failed to fetch stats:", error);
    }
  }

  useEffect(() => {
    // Fetch immediately
    fetchStats();
    
    // Poll every 2 seconds
    const interval = setInterval(fetchStats, 2000);
    return () => clearInterval(interval);
  }, []);

  // Helper to convert bytes to GB
  const toGB = (bytes: number) => (bytes / 1024 / 1024 / 1024).toFixed(2);

  return (
    <div className="container">
      <h1>Rust System Monitor</h1>
      
      {stats ? (
        <div className="card">
          <div className="stat-row">
            <span>Total Memory:</span>
            <strong>{toGB(stats.total_memory)} GB</strong>
          </div>
          <div className="stat-row">
            <span>Used Memory:</span>
            <strong>{toGB(stats.used_memory)} GB</strong>
          </div>
          <div className="stat-row">
            <span>Usage:</span>
            <div className="progress-bar-bg">
              <div 
                className="progress-bar-fill" 
                style={{ width: `${stats.usage_percentage}%` }}
              ></div>
            </div>
            <strong>{stats.usage_percentage}%</strong>
          </div>
        </div>
      ) : (
        <p>Loading system data...</p>
      )}
    </div>
  );
}

export default App;

3.2 Basic Styling
#

Add some CSS to src/App.css to make it look decent.

/* src/App.css */
.container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100vh;
  background-color: #f0f0f0;
  color: #333;
  font-family: sans-serif;
}

.card {
  background: white;
  padding: 2rem;
  border-radius: 12px;
  box-shadow: 0 4px 6px rgba(0,0,0,0.1);
  width: 300px;
}

.stat-row {
  display: flex;
  justify-content: space-between;
  margin-bottom: 1rem;
  align-items: center;
}

.progress-bar-bg {
  width: 100px;
  height: 10px;
  background-color: #eee;
  border-radius: 5px;
  overflow: hidden;
  margin-right: 10px;
}

.progress-bar-fill {
  height: 100%;
  background-color: #ff6b6b; /* Rust color-ish */
  transition: width 0.5s ease-in-out;
}

Running the Application
#

Now for the moment of truth. Run the development server:

pnpm tauri dev

This command will:

  1. Compile the Rust backend.
  2. Start the Vite frontend server.
  3. Launch a native window containing your app.

You should see a clean, native-feeling window updating memory usage every two seconds. The initial build might take a minute, but subsequent reloads are instant thanks to Hot Module Replacement (HMR).


Comparison: Tauri vs. The Rest
#

Why go through the trouble of learning Rust integration? Let’s look at the numbers.

Feature Tauri Electron Native (Iced/Slint)
Binary Size < 5 MB 80 MB - 150 MB+ < 5 MB
RAM Usage (Idle) ~30 MB ~150 MB+ ~10 MB
Backend Language Rust Node.js Rust
Frontend Tech Web (JS/TS/CSS) Web (JS/TS/CSS) Rust Widgets
Startup Time Instant Slow Instant
Security High (Rust + Isolation) Moderate (Sandbox) High

Analysis: Tauri hits the sweet spot. It offers the Developer Experience (DX) of web development with the performance profile of a native application. While pure native frameworks like Iced are even lighter, they lack the vast ecosystem of UI libraries available to React/Vue.


Best Practices & Performance Optimization
#

1. The Async Trap
#

In our Rust code, the command function get_memory_usage is synchronous. For simple memory reads, this is fine. However, if you perform I/O (file reading, network requests) synchronously in a command, you will freeze the UI.

Solution: Always make potentially slow commands async. Tauri handles the Future automatically.

#[tauri::command]
async fn expensive_calculation() -> String {
    // Perform heavy work here
    "Done".to_string()
}

2. Payload Size
#

Communication between Rust and JS involves serialization (JSON).

  • Good: Sending status flags, small structs, configuration.
  • Bad: Sending a 10MB raw image buffer as a JSON array of numbers.
  • Fix: For large binary data, use Tauri’s sidecar pattern or dedicated binary channels instead of standard JSON serialization.

3. Security Allowlist
#

Tauri 2.0 introduces a stricter permissions model. In your src-tauri/capabilities configuration, only enable the APIs you actually use. Do not use wildcards * in production. This drastically reduces the attack surface if your frontend is compromised via XSS.


Conclusion
#

We have successfully built a cross-platform desktop application that monitors system resources, utilizing React for the UI and Rust for the heavy lifting.

The code we wrote today compiles down to a binary that is likely under 4MB—a fraction of the size of a similar Electron app. By leveraging Rust’s ownership model, we also ensure memory safety and concurrency without data races.

Production Note: When you are ready to ship, run pnpm tauri build. This produces an optimized release binary and an installer (.msi, .dmg, or .deb) ready for distribution.

Further Reading
#

  • Tauri V2 Documentation: Dive into the new capabilities system.
  • Rust Async Book: Essential for handling complex backend logic.
  • Trunk / Leptos: If you want to go “Full Rust” and ditch JavaScript entirely in the frontend (Wasm).

Start building today. The desktop is open for business again.