Tauri — Desktop App Với Web Technology Nhẹ Hơn Electron
Tauri xây desktop app bằng web tech (HTML/CSS/JS) nhưng nhẹ hơn Electron 20x. Rust backend, bundle ~2MB, bảo mật hơn.
Tauri là framework xây desktop app với web technology — như Electron nhưng nhẹ hơn, nhanh hơn, an toàn hơn.
Electron: Chromium (~200MB) + Node.js + HTML/CSS/JS
Tauri: WebView (OS-native) + Rust backend + HTML/CSS/JSplaintextSize: Electron app ~200MB. Tauri app ~2MB.
Tại Sao Tauri?#
So Sánh Với Electron#
| Electron | Tauri | |
|---|---|---|
| Bundle size | ~200MB | ~2MB |
| Memory | ~100-200MB | ~20-50MB |
| Backend | Node.js | Rust |
| Security | Thấp (Node full access) | Cao (allowlist API) |
| Build | npm build | npm build + cargo build |
| Cross-platform | ✅ | ✅ (macOS, Windows, Linux) |
| Mobile | ❌ | ✅ (iOS, Android — Tauri v2) |
Tauri v2#
Tauri v2 (2025+) hỗ trợ mobile — iOS và Android:
Tauri v1: Desktop (macOS, Windows, Linux)
Tauri v2: Desktop + Mobile (iOS, Android)plaintextCài Đặt#
# Prerequisites
# macOS: Xcode
# Linux: WebKitGTK, libsoup
# Windows: WebView2
npm create tauri-app@latest my-app
# Select: React, Vue, Svelte, Vanilla...
cd my-app
npm install
npm run tauri devbashProject Structure#
my-app/
├── src/ # Frontend (React/Vue/Svelte)
├── src-tauri/
│ ├── src/
│ │ └── main.rs # Rust backend
│ ├── Cargo.toml
│ ├── tauri.conf.json
│ └── icons/
└── package.jsonplaintextFrontend (React + Vite)#
// src/App.tsx — Frontend React
import { invoke } from '@tauri-apps/api/core'
import { useState } from 'react'
function App() {
const [count, setCount] = useState(0)
const [rustMessage, setRustMessage] = useState('')
const greet = async () => {
// Gọi Rust function
const msg = await invoke('greet', { name: 'Tauri' })
setRustMessage(msg as string)
}
return (
<div>
<h1>Welcome to Tauri!</h1>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<button onClick={greet}>Call Rust</button>
<p>{rustMessage}</p>
</div>
)
}tsxRust Backend#
// src-tauri/src/main.rs
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! From Rust 🦀", name)
}
#[tauri::command]
fn add(a: i32, b: i32) -> i32 {
a + b
}
#[tauri::command]
fn read_file(path: &str) -> Result<String, String> {
std::fs::read_to_string(path)
.map_err(|e| e.to_string())
}
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet, add, read_file])
.run(tauri::generate_context!())
.expect("error while running tauri application")
}rustTauri Commands (IPC)#
Frontend gọi Rust backend qua invoke:
// Frontend
import { invoke } from '@tauri-apps/api/core'
// Gọi command
const sum = await invoke('add', { a: 5, b: 3 })
// sum: 8
// Với error handling
try {
const content = await invoke('read_file', { path: '/etc/hosts' })
console.log(content)
} catch (error) {
console.error('Failed to read file:', error)
}typescript// Rust — command có thể return Result
#[tauri::command]
fn get_user(id: u32) -> Result<User, String> {
let user = database::find_user(id)
.ok_or_else(|| format!("User {} not found", id))?;
Ok(user)
}rustFile System#
import { readTextFile, writeTextFile } from '@tauri-apps/plugin-fs'
import { resolve } from '@tauri-apps/api/path'
// Đọc/ghi file trong app data directory
const appDir = await resolve(appDataDir())
const configPath = await resolve(appDir, 'config.json')
// Đọc
const config = await readTextFile(configPath)
// Ghi
await writeTextFile(configPath, JSON.stringify({ theme: 'dark' }))typescriptSystem Tray#
use tauri::tray::TrayIconBuilder;
use tauri::menu::{Menu, MenuItem};
fn main() {
let tray_menu = Menu::with_items([
MenuItem::with_id("show", "Show")?,
MenuItem::with_id("quit", "Quit")?,
])?;
tauri::Builder::default()
.setup(|app| {
TrayIconBuilder::new()
.icon(app.default_window_icon().unwrap().clone())
.menu(&tray_menu)
.on_menu_event(|app, event| {
match event.id.as_ref() {
"quit" => app.exit(0),
"show" => { /* show window */ }
_ => {}
}
})
.build(app)?;
Ok(())
})
.run(tauri::generate_context!())
}rustWindow Management#
#[tauri::command]
async fn create_window(app: tauri::AppHandle, label: String) {
let _window = tauri::WebviewWindowBuilder::new(
&app,
&label,
tauri::WebviewUrl::App("index.html".into()),
)
.title("New Window")
.inner_size(800.0, 600.0)
.build()
.expect("Failed to create window");
}rust// Frontend
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
const win = new WebviewWindow('settings', {
url: '/settings',
width: 600,
height: 400,
title: 'Settings',
})
win.once('tauri://created', () => {
console.log('Window created')
})
win.once('tauri://error', (e) => {
console.error('Failed:', e)
})typescriptPlugin Ecosystem#
# Tauri plugins
npm run tauri add shell # Run shell commands
npm run tauri add fs # File system
npm run tauri add dialog # Native dialogs
npm run tauri add http # HTTP client
npm run tauri add store # Persistent key-value store
npm run tauri add sql # SQLite
npm run tauri add clipboard # Clipboard access
npm run tauri add updater # Auto-updatebashimport { open } from '@tauri-apps/plugin-dialog'
// Native file dialog
const file = await open({
multiple: false,
filters: [{ name: 'Images', extensions: ['png', 'jpg'] }],
})typescriptimport { Store } from '@tauri-apps/plugin-store'
// Persistent store
const store = await Store.load('settings.json')
await store.set('theme', 'dark')
await store.save()
const theme = await store.get<string>('theme')typescriptBuild & Distribute#
# Development
npm run tauri dev
# Build cho production
npm run tauri build
# dist/tauri/my-app.dmg (macOS)
# dist/tauri/my-app.msi (Windows)
# dist/tauri/my-app.deb (Linux)
# ~2MB — so với Electron ~200MBbash// tauri.conf.json — bundle config
{
"bundle": {
"active": true,
"icon": ["icons/icon.png"],
"identifier": "com.myapp.app",
"targets": ["dmg", "msi", "deb", "appimage"]
},
"security": {
"csp": "default-src 'self'; script-src 'self'"
}
}jsonAuto-Update#
{
"plugins": {
"updater": {
"pubkey": "...",
"endpoints": [
"https://releases.myapp.com/{{target}}-{{arch}}/{{current_version}}"
]
}
}
}jsonimport { check } from '@tauri-apps/plugin-updater'
const update = await check()
if (update?.available) {
await update.downloadAndInstall()
}typescriptKhi Nào Dùng Tauri?#
✅ Nên dùng#
- Desktop app mới — muốn nhẹ, fast, native
- Cross-platform — một codebase cho macOS, Windows, Linux
- System tool — cần Rust performance (file processing, network)
- Mobile + Desktop — Tauri v2 cho iOS và Android
- Security-sensitive — cần sandbox, minimal API access
❌ Không nên#
- Desktop app cần nhiều Node.js library — Electron ecosystem lớn hơn
- Complex native API — Electron có nhiều native module hơn
- Prototype nhanh — Electron quen hơn nếu team biết Node
- Cần Chromium DevTools mạnh — WebView ít tính năng hơn
Tổng Kết#
Tauri là Electron thế hệ mới: web frontend, Rust backend, bundle 1/100 kích thước.
# Tauri app — ~2MB vs Electron ~200MB
npm create tauri-app@latest my-app
cd my-app
npm run tauri dev # Dev với hot reload
npm run tauri build # Bundle ~2MB .dmg/.msi/.debbash| Electron | Tauri | |
|---|---|---|
| Bundle | ~200MB | ~2MB |
| RAM | ~100-200MB | ~20-50MB |
| Backend | Node.js | Rust 🦀 |
| Security | Thấp | Cao (allowlist) |
| Mobile | ❌ | ✅ (Tauri v2) |
| Build time | 1-2 phút | 2-5 phút (+ Rust compile) |
Tauri không thay thế Electron mọi lúc — nhưng cho 80% desktop app, bundle nhỏ hơn 100x, memory ít hơn 5x là lợi thế khó bỏ qua.