Tauri vs Electron vs Neutralino: Desktop Apps with JavaScript 2026
Tauri vs Electron vs Neutralino: Desktop Apps with JavaScript 2026
TL;DR
Electron remains the most battle-tested choice — it ships Chromium and Node.js together, so it just works, and the ecosystem is enormous (VS Code, Slack, Figma, Discord all run on it). Tauri is the modern challenger — it uses the OS's native WebView and a Rust backend, resulting in 10–20x smaller bundles and dramatically lower memory usage. Neutralino is the lean option — no native backend runtime required at all, just a thin OS wrapper around a WebView.
Key Takeaways
- Electron app bundles average 80–150 MB vs Tauri's 3–10 MB vs Neutralino's 1–2 MB (app logic only)
- Memory at idle: Electron ~200MB, Tauri ~30MB, Neutralino ~20MB — Tauri's Rust backend is dramatically lighter
- Electron has 115k+ GitHub stars — the most mature ecosystem with thousands of packages
- Tauri reached 85k+ stars (2026) — fastest-growing desktop framework in the JS ecosystem
- Neutralino has 7.5k stars — niche but solid for simple tools where Node.js APIs aren't needed
- Tauri's security model is significantly stronger — Rust backend, allowlist-based API permissions, no Node.js attack surface
- Electron has the largest plugin ecosystem — electron-builder, electron-updater, electron-store all have millions of downloads
Why Desktop Still Matters in 2026
Progressive Web Apps haven't killed desktop apps. They've clarified the niche: desktop wins when apps need deep OS integration (file system access, system tray, hardware devices, clipboard, global shortcuts), offline-first operation, or consistent behavior across OS versions without browser compatibility constraints.
The battle is no longer "native vs web" — it's about which JS-based desktop runtime gives you the right tradeoffs between bundle size, performance, security, and access to native APIs.
Electron: The Proven Standard
Electron bundles Chromium (the full browser engine) and Node.js into your app distribution. Every Electron app ships a complete browser — which explains both its strengths (full web API parity, any npm package works) and its weaknesses (file size, memory).
Project Setup
# Create with Electron Forge (the official scaffolding tool)
npm create electron-app@latest my-app -- --template=vite-typescript
cd my-app && npm start
Main Process (Node.js)
// main.ts — runs in Node.js context
import { app, BrowserWindow, ipcMain, dialog, shell } from "electron";
import { join } from "path";
import Store from "electron-store";
import { autoUpdater } from "electron-updater";
const store = new Store<{ windowBounds: { width: number; height: number } }>();
let mainWindow: BrowserWindow | null = null;
function createWindow() {
const { width, height } = store.get("windowBounds", { width: 1200, height: 800 });
mainWindow = new BrowserWindow({
width,
height,
titleBarStyle: "hiddenInset",
webPreferences: {
preload: join(__dirname, "preload.js"),
contextIsolation: true, // Security: never disable this
nodeIntegration: false, // Security: never enable this
},
});
if (process.env.NODE_ENV === "development") {
mainWindow.loadURL("http://localhost:5173");
mainWindow.webContents.openDevTools();
} else {
mainWindow.loadFile(join(__dirname, "renderer/index.html"));
autoUpdater.checkForUpdatesAndNotify();
}
mainWindow.on("close", () => {
store.set("windowBounds", mainWindow!.getBounds());
});
}
app.whenReady().then(createWindow);
// IPC handlers — bridge between renderer and Node.js
ipcMain.handle("dialog:openFile", async () => {
const { filePaths } = await dialog.showOpenDialog(mainWindow!, {
properties: ["openFile"],
filters: [{ name: "JSON Files", extensions: ["json"] }],
});
return filePaths[0];
});
ipcMain.handle("shell:openExternal", (_event, url: string) => {
return shell.openExternal(url);
});
Preload Script (Security Bridge)
// preload.ts — context bridge between renderer and main
import { contextBridge, ipcRenderer } from "electron";
contextBridge.exposeInMainWorld("electronAPI", {
openFile: () => ipcRenderer.invoke("dialog:openFile"),
openExternal: (url: string) => ipcRenderer.invoke("shell:openExternal", url),
onUpdateAvailable: (callback: () => void) =>
ipcRenderer.on("update-available", callback),
});
// TypeScript declaration for renderer
declare global {
interface Window {
electronAPI: {
openFile: () => Promise<string | undefined>;
openExternal: (url: string) => Promise<void>;
onUpdateAvailable: (callback: () => void) => void;
};
}
}
Renderer Process (React/Vue/etc)
// App.tsx — runs in Chromium
import { useState } from "react";
export function App() {
const [file, setFile] = useState<string | null>(null);
const handleOpenFile = async () => {
const filePath = await window.electronAPI.openFile();
if (filePath) setFile(filePath);
};
return (
<div>
<button onClick={handleOpenFile}>Open File</button>
{file && <p>Selected: {file}</p>}
</div>
);
}
Auto-Update Configuration
// electron-builder.config.js
module.exports = {
appId: "com.yourcompany.app",
productName: "My App",
publish: {
provider: "github",
owner: "your-org",
repo: "your-app",
},
mac: { category: "public.app-category.developer-tools" },
win: { target: "nsis" },
linux: { target: ["AppImage", "deb"] },
};
Tauri: The Modern Challenger
Tauri uses the OS's built-in WebView (WebKit on macOS/Linux, WebView2 on Windows) for the frontend, and a Rust binary for the backend. No bundled Chromium means dramatically smaller apps — but it requires Rust toolchain for development.
Project Setup
# Prerequisites: Rust toolchain
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Create Tauri app
npm create tauri-app@latest my-tauri-app
# Select: Vite + TypeScript + React (or Vue/Svelte)
cd my-tauri-app
npm install
npm run tauri dev
Rust Backend (src-tauri/src/main.rs)
// src-tauri/src/main.rs
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use tauri::{Manager, State};
use std::sync::Mutex;
// Shared state across commands
struct AppState {
counter: Mutex<u32>,
}
// Commands are type-safe functions exposed to the frontend
#[tauri::command]
async fn read_file(path: String) -> Result<String, String> {
tokio::fs::read_to_string(&path)
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
fn increment_counter(state: State<AppState>) -> u32 {
let mut counter = state.counter.lock().unwrap();
*counter += 1;
*counter
}
#[tauri::command]
async fn fetch_data(url: String) -> Result<String, String> {
// Tauri's fetch is separate from WebView's fetch
// This runs in Rust — bypasses CORS restrictions
let response = reqwest::get(&url)
.await
.map_err(|e| e.to_string())?
.text()
.await
.map_err(|e| e.to_string())?;
Ok(response)
}
fn main() {
tauri::Builder::default()
.manage(AppState { counter: Mutex::new(0) })
.invoke_handler(tauri::generate_handler![
read_file,
increment_counter,
fetch_data,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
TypeScript Frontend Integration
// Tauri's invoke is fully type-safe with @tauri-apps/api
import { invoke } from "@tauri-apps/api/core";
import { open } from "@tauri-apps/plugin-dialog";
import { readTextFile, writeTextFile } from "@tauri-apps/plugin-fs";
import { emit, listen } from "@tauri-apps/api/event";
// Call Rust commands
const count = await invoke<number>("increment_counter");
console.log("Counter:", count);
// File operations via official plugins
const filePath = await open({
multiple: false,
filters: [{ name: "JSON", extensions: ["json"] }],
});
if (filePath) {
const contents = await readTextFile(filePath as string);
console.log("File contents:", contents);
}
// Listen to events from Rust backend
const unlisten = await listen<{ message: string }>("backend-event", (event) => {
console.log("Event from Rust:", event.payload.message);
});
// Emit events to Rust backend
await emit("frontend-action", { action: "save", data: "..." });
Tauri Permissions Model (tauri.conf.json)
{
"app": {
"security": {
"csp": "default-src 'self'; img-src 'self' data: https:"
}
},
"plugins": {
"fs": {
"scope": {
"allow": ["$APPDATA/**", "$DOCUMENT/**"],
"deny": ["$APPDATA/sensitive/**"]
}
},
"dialog": {
"all": true
},
"http": {
"scope": ["https://api.yourservice.com/**"]
}
}
}
Tauri Plugin System
# Official plugin ecosystem
npm install @tauri-apps/plugin-fs
npm install @tauri-apps/plugin-dialog
npm install @tauri-apps/plugin-notification
npm install @tauri-apps/plugin-shell
npm install @tauri-apps/plugin-store # Persistent key-value storage
npm install @tauri-apps/plugin-updater # Auto-updates
npm install @tauri-apps/plugin-sql # SQLite via sqlx
Neutralino: The Minimal Option
Neutralino provides a native OS window (or system tray) around a WebView, exposing a minimal set of OS APIs via a built-in REST/WebSocket server. No Rust, no Node.js — just a tiny binary and your web app.
Project Setup
# Install Neutralino CLI
npm install -g @neutralinojs/neu
# Create project
neu create my-app --template neutralinojs/neutralinojs-minimal
cd my-app && neu run
Configuration (neutralino.config.json)
{
"applicationId": "js.neutralino.myapp",
"version": "1.0.0",
"defaultMode": "window",
"port": 0,
"documentRoot": "/resources/",
"url": "/",
"enableServer": true,
"enableNativeAPI": true,
"tokenSecurity": "one-time",
"logging": {
"enabled": true,
"writeToLogFile": true
},
"nativeAllowList": [
"app.*",
"os.*",
"filesystem.*",
"storage.*",
"window.*"
],
"modes": {
"window": {
"title": "My Neutralino App",
"width": 1200,
"height": 800,
"minWidth": 800,
"minHeight": 600,
"resizable": true,
"maximize": false
}
},
"cli": {
"binaryName": "myapp",
"resourcesPath": "/resources/",
"extensionsPath": "/extensions/",
"frontendLibrary": {
"patchFile": "/resources/index.html",
"devUrl": "http://localhost:5173"
}
}
}
Native API Usage
// Neutralino's built-in API — no npm packages needed
import Neutralino from "@neutralinojs/lib";
Neutralino.init();
// File system
const data = await Neutralino.filesystem.readFile("./data/config.json");
const parsed = JSON.parse(data);
await Neutralino.filesystem.writeFile(
"./data/output.json",
JSON.stringify({ result: "ok" }, null, 2)
);
// OS operations
const info = await Neutralino.os.getEnv("HOME");
const pid = await Neutralino.os.execCommand("ls -la /tmp", {
background: false,
stdIn: "",
});
console.log(pid.stdOut);
// Storage (simple key-value, no npm needed)
await Neutralino.storage.setData("user-preferences", JSON.stringify({ theme: "dark" }));
const prefs = await Neutralino.storage.getData("user-preferences");
// Window management
await Neutralino.window.setTitle("Updated Title");
await Neutralino.window.setSize({ width: 1400, height: 900 });
await Neutralino.window.center();
Extensions (Custom Backend Logic)
// extensions/backend/main.js — Node.js or Python process you spawn
// Neutralino doesn't have a built-in backend runtime
// For complex logic, write extensions:
const NeutralinoExtension = require("@neutralinojs/neu-extension");
const ext = new NeutralinoExtension();
ext.on("initExtension", () => {
console.log("Extension initialized");
});
ext.on("customLogic.processData", async (event) => {
const { data } = event.detail;
// Do compute-intensive work here
const result = heavyComputation(data);
await ext.sendMessage("customLogic.result", { result });
});
ext.init();
Feature Comparison
| Feature | Electron | Tauri | Neutralino |
|---|---|---|---|
| Bundle size | 80–150 MB | 3–10 MB | 1–2 MB |
| Memory at idle | ~200 MB | ~30 MB | ~20 MB |
| Backend runtime | Node.js | Rust | None (extensions possible) |
| WebView | Bundled Chromium | OS WebView | OS WebView |
| Cross-platform | ✅ Win/Mac/Linux | ✅ Win/Mac/Linux | ✅ Win/Mac/Linux |
| Auto-update | ✅ electron-updater | ✅ plugin-updater | ❌ (manual) |
| TypeScript support | ✅ | ✅ (frontend) | ✅ (frontend) |
| NPM packages | ✅ All packages | ✅ Frontend only | ✅ Frontend only |
| Native APIs depth | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| Security model | Medium (IPC bridge) | Strong (Rust, allowlist) | Medium (token-based) |
| Build tooling | electron-builder | Cargo + Vite | neu CLI |
| Dev experience | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| Ecosystem maturity | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ |
| GitHub stars | 115k+ | 85k+ | 7.5k+ |
Performance Benchmarks
Real-world measurements from community benchmarks (2025–2026):
| Metric | Electron | Tauri | Neutralino |
|---|---|---|---|
| Cold start | 1.8–3s | 0.4–0.8s | 0.3–0.6s |
| Memory (idle) | 150–250 MB | 25–50 MB | 15–30 MB |
| CPU (idle) | 1–3% | 0.1–0.5% | 0.1–0.3% |
| Min bundle (macOS) | 120 MB | 5 MB | 1.5 MB |
| File I/O speed | Fast (Node.js streams) | Fast (Rust async) | Moderate |
| App launch (warm) | 600ms | 200ms | 150ms |
Benchmarks vary by hardware and app complexity. Values are typical ranges from community testing.
When to Use Each
Choose Electron if:
- Your team knows JavaScript/Node.js deeply and doesn't want to learn Rust
- You need the entire npm ecosystem accessible in your backend logic
- Bundle size doesn't matter (internal tools, B2B apps)
- You're building something like a code editor, database GUI, or design tool with complex native integrations
- The mature
electron-builder,electron-updater, andelectron-storeecosystem matters
Choose Tauri if:
- Bundle size and memory usage are important to end users (consumer-facing apps)
- You want strong security by default — Rust's memory safety, no global
requirein renderer - Your team can invest in learning Rust (or has Rust expertise)
- You're building a productivity tool, file manager, or anything where startup speed matters
- Cross-compilation support is important (Tauri can compile for all platforms from CI)
Choose Neutralino if:
- You're building a simple utility or internal tool with minimal native API needs
- Bundle size is paramount (distributing a tiny CLI-wrapper with a UI)
- You don't need Node.js or Rust at all — just a WebView window around a web app
- You already have a backend service and just need an OS window
Skip all three if:
- Your users are always online and a PWA with
navigatorAPIs is sufficient - You only need one OS (use Swift/AppKit for macOS, WinUI for Windows)
Ecosystem Signals
Tauri's trajectory is remarkable: from 25k stars in 2022 to 85k in 2026, with an active plugin ecosystem and strong Rust community backing. The v2 release stabilized the API and improved Windows support (finally using WebView2 reliably). Enterprise adoption is growing — Tauri is increasingly used for internal tools at companies that care about security posture.
Electron isn't going anywhere. VS Code alone justifies its existence. But new projects starting in 2026 should have a strong reason to pick Electron over Tauri if bundle size or memory matters.
Neutralino is a legitimate niche tool. Don't dismiss it — for the specific case of "I want my web app to open in a native OS window with some file access," it's the right tool.
Methodology
Data from GitHub repositories, official documentation, npm download statistics, and community benchmarks from the Tauri Discord and Electron community forums. Star counts as of February 2026. Bundle sizes measured with minimal hello-world apps on macOS Sequoia. Memory benchmarks measured with Activity Monitor on M2 MacBook Pro. Pricing: all three frameworks are free and open source.
For cross-platform mobile (not desktop), see React Native vs Expo vs Capacitor. For build tooling comparisons, explore our Vite vs Webpack vs esbuild analysis.