DEV Community

BAOFUFAN
BAOFUFAN

Posted on

I Replaced Electron with Tauri – Memory Dropped 90%, Bundle Size to 5MB

At 2 AM, a colleague dropped a screenshot of Task Manager in the group chat: a "todo list" desktop app with 17 task cards was eating 540 MB of RAM. Our boss chimed in, "How is our tool hungrier than an IDE?" We'd been using an Electron-based Todo app that took 8 seconds to start and had noticeable delays switching tabs. People had complained three months ago, and now a screenshot pinned it to the wall of shame. That weekend I decided to rewrite it with Tauri + SQLite. The result? Memory dropped to 46 MB and the installer shrunk from 118 MB to 4.7 MB — a leap worth talking about.


Breaking down the problem

Electron's pain boils down to one thing: every app is a mini Chromium browser. Even if you just want to display "You did nothing today," it still needs to spin up the full renderer process, V8 engine, and Node.js runtime. Our Todo app had a list with rich text editing and used better-sqlite3 for storage — a nice developer experience, but the performance bill was handed entirely to the users:

  • Memory: a single empty Electron window consumes ~250 MB; as soon as actual data loads, it easily exceeds 500 MB.
  • Bundle size: even with just a few thousand lines of JS, the package starts at 100 MB+ because the Chromium stack must be included.
  • Startup time: a cold start loads Node and the rendering engine, taking 8 seconds — enough for a user to smash their mouse three times.

Electron's "write desktop apps with web tech" really is sweet — sweet for developers, bitter for users. The team considered switching to CEF or going native, but CEF adds significant complexity and native would mean maintaining two separate codebases. We needed a middle ground: keep the frontend ecosystem, but replace the bloated browser runtime.


Designing the solution

The first candidate to get the ax was Flutter. Not because it's bad, but the team’s frontend stack is all React/TypeScript — switching to Dart had a steep learning curve and an uncontrollable migration cost.

Next we looked at Neutralinojs. It uses the system WebView and does have a tiny footprint, but its ecosystem and community aren't mature enough yet. Hitting a weird bug would mean digging through the source alone — too risky.

In the end we picked Tauri for a few clear reasons:

  • System WebView instead of Chromium: Windows uses Edge WebView2, macOS uses WKWebView. No more bundling a browser per app; bundle size drops from the 100 MB+ range to under 5 MB.
  • Rust backend: all core logic runs in Rust with native performance and precise memory control. SQLite can be accessed directly through the rusqlite crate, bypassing the serialization overhead of Node C++ addons.
  • Minimal IPC: the frontend calls Rust commands with invoke, data passes as JSON — no heavy context isolation between renderer and main processes.
  • Security model: Tauri disables Node.js APIs by default; the frontend can't casually access the filesystem, making the security boundary much cleaner than Electron's.

Architecturally, we rewrote the todo app with Tauri + React + SQLite: the frontend handles only UI; all database reads/writes and file operations are wrapped as Tauri commands executed on the Rust side. The SQLite database file lives in the app data directory — single-file, zero-config deployment, perfectly suited for offline use.


Core implementation

Here's a step-by-step walkthrough with runnable code.

1. Create the Tauri project and add dependencies

This solves the "scaffold from scratch" problem. We're using the React + TypeScript template:

npm create tauri-app@latest todo-tauri -- --template react-ts
cd todo-tauri
Enter fullscreen mode Exit fullscreen mode

In src-tauri/Cargo.toml, add rusqlite (with the bundled feature to compile SQLite in, avoiding system dependency headaches) and serde (for command argument/return serialization):

[dependencies]
tauri = { version = "1.5", features = ["shell-open"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
rusqlite = { version = "0.30", features = ["bundled"] }
Enter fullscreen mode Exit fullscreen mode

2. Initialize the database connection, protect with Mutex

This code solves "hold SQLite connection safely and thread‑safely in Rust". rusqlite::Connection is not Send, so you can't just place it into Tauri commands directly. I wrap it with std::sync::Mutex and manage it via tauri::State.

src-tauri/src/main.rs:

// 防止 Windows 上缺少 WebView2 时静默失败
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

use rusqlite::Connection;
use serde::{Deserialize, Serialize};
use std::sync::Mutex;
use tauri::State;

// 定义待办条目的数据结构,前后端共享
#[derive(Debug, Serialize, Deserialize, Clone)]
struct Todo {
    id: Option<i64>,
    title: String,
    completed: bool,
}

struct DbState(Mutex<Connection>);

#[tauri::command]
fn add_todo(state: State<DbState>, title: String) -> Result<Todo, String> {
    let conn = state.0.lock().map_err(|e| e.to_string())?;
    conn.execute("INSERT INTO todos (title, completed) VALUES (?1, 0)", [&title])
        .map_err(|e| e.to_string())?;
    let id = conn.last_insert_rowid();
    Ok(Todo {
        id: Some(id),
        title,
        completed: false,
    })
}

#[tauri::command]
fn get_todos(state: State<DbState>) -> Result<Vec<Todo>, String> {
Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
marcusykim profile image
Marcus Kim

Desktop app work makes tradeoffs visible fast. Keeping the React UI while moving heavy local work into Tauri/Rust is a practical example of changing the system shape without rewriting the entire product idea.