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)
WebView Rendering Differences Across Platforms
One of the most important practical differences between Electron and the OS-WebView-based frameworks (Tauri, Neutralino) is rendering consistency. Electron ships its own Chromium version, so your app renders identically on Windows, macOS, and Linux — what you see in Chrome is what you get. Tauri and Neutralino use the operating system's built-in WebView, which differs significantly across platforms.
On macOS, Tauri uses WKWebView (WebKit) — the same engine as Safari. WebKit has excellent performance and supports modern web standards, but it is noticeably behind Chrome/Chromium on some cutting-edge features and has historically had CSS rendering differences (particularly with certain flex behaviors, scroll snap, and complex CSS Grid patterns). If your application uses CSS features that Safari lags on, you'll encounter rendering issues in Tauri on macOS. On Windows, Tauri uses WebView2 (Microsoft's Chromium-based WebView), which is much closer to Chrome's rendering. On Linux, Tauri uses WebKitGTK, which has more variability depending on the distribution and installed library versions.
The practical consequence is that Tauri applications require cross-platform browser testing even if you develop on macOS — your CSS must work in both WebKit (macOS/Linux) and WebView2 (Windows). Electron eliminates this matrix by pinning to a specific Chromium version, at the cost of shipping that Chromium to every user. For applications using modern CSS features (container queries, @layer, complex has() selectors), always test on both Safari/WebKit and Chrome to catch rendering differences before shipping your Tauri app.
Distribution, Code Signing, and Auto-Updates
Getting a desktop app into users' hands requires platform-specific code signing, notarization, and distribution infrastructure. The tooling for each framework differs significantly and affects how much time you spend on DevOps versus product development.
Electron's electron-builder handles code signing for all three platforms through environment variables: CSC_LINK and CSC_KEY_PASSWORD for macOS/Windows certificates. On macOS, Electron apps must be notarized with Apple's notary service before Gatekeeper will allow them to run on user machines — electron-builder automates this with the --notarize flag when APPLE_ID and APPLE_APP_SPECIFIC_PASSWORD are set. electron-updater provides auto-update functionality that integrates with GitHub Releases, S3, or a custom server, implementing delta updates to minimize download size for users already on a recent version.
Tauri's distribution story uses Cargo-based cross-compilation and tauri-build for signing. macOS notarization is handled through Tauri's action in CI: tauri-apps/tauri-action. Tauri's updater plugin implements end-to-end encrypted update signatures using ed25519 — your private key signs the update payload, and the app verifies it before applying. This means a compromised update server cannot push malicious updates without your private key, a security guarantee that Electron's auto-update system requires additional configuration to achieve.
Neutralino lacks built-in auto-update infrastructure, which is its most significant production limitation. Apps that need silent background updates must implement this themselves (typically by checking a version endpoint and downloading/applying the new binary). For utility tools where manual updates are acceptable, this is not a blocking issue. For consumer-facing apps that need zero-friction updates — the kind VS Code or Figma provide — Neutralino is not the right foundation.
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.
Compare Electron and Tauri package health on PkgPulse.