Skip to main content

Guide

Tauri vs Electron vs Neutralino 2026: Bundles, Memory & Which to Pick

Electron vs Tauri vs Neutralino: Tauri's 3 MB vs Electron's 150 MB. Bundle sizes, memory usage, native APIs, and which desktop framework to pick in 2026.

·PkgPulse Team·
0

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

FeatureElectronTauriNeutralino
Bundle size80–150 MB3–10 MB1–2 MB
Memory at idle~200 MB~30 MB~20 MB
Backend runtimeNode.jsRustNone (extensions possible)
WebViewBundled ChromiumOS WebViewOS 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 modelMedium (IPC bridge)Strong (Rust, allowlist)Medium (token-based)
Build toolingelectron-builderCargo + Viteneu CLI
Dev experience⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Ecosystem maturity⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
GitHub stars115k+85k+7.5k+

Performance Benchmarks

Real-world measurements from community benchmarks (2025–2026):

MetricElectronTauriNeutralino
Cold start1.8–3s0.4–0.8s0.3–0.6s
Memory (idle)150–250 MB25–50 MB15–30 MB
CPU (idle)1–3%0.1–0.5%0.1–0.3%
Min bundle (macOS)120 MB5 MB1.5 MB
File I/O speedFast (Node.js streams)Fast (Rust async)Moderate
App launch (warm)600ms200ms150ms

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, and electron-store ecosystem 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 require in 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 navigator APIs 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.

Related: Electron vs Tauri 2026: 120MB vs 8MB Bundle.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.