Electron.js 2026: Production Desktop Apps π
Electron v39 (Chromium 142, Node 22.20, V8 13.8) powers VS Code, Discord, Slack (90% top desktop apps). Single JS codebase β Windows/macOS/Linux with native menus, system tray, auto-updates, file system access, crash reporting. React/Vue/Svelte + Vite + TypeScript = enterprise desktop.
[image:352]
π― Electron vs Tauri vs Flutter Desktop
| Feature | Electron v39 | Tauri v2 | Flutter Desktop |
|---|---|---|---|
| Bundle Size | 120MB | 4MB | 25MB |
| Performance | WebView | Native | Native |
| Dev Speed | Fastest | Medium | Medium |
| Ecosystem | Massive | Rust | Dart |
| Hot Reload | Yes | Yes | Yes |
| Native APIs | Full Node | Rust FFI | Platform channels |
π Quick Start (3 Minutes)
# Modern template (React + Vite + TS)
npm create electron-vite@latest my-desktop-app -- --template react-ts
cd my-desktop-app
npm install
npm run dev # Hot reload β
npm run build # Production build
npm run preview # Packaged app
my-app/ βββ src/ β βββ main/ # Node.js (main process) β βββ preload/ # IPC bridge β βββ renderer/ # React UI βββ electron.vite.config.ts βββ electron-builder.config.ts
ποΈ Production Architecture
1. Main Process (Node.js + TypeScript)
// src/main/index.ts
import { app, BrowserWindow, ipcMain, Menu, shell, Tray } from 'electron';
import * as path from 'path';
class Main {
private win: BrowserWindow | null = null;
private tray: Tray | null = null;
constructor() {
app.whenReady().then(() => this.init());
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
}
private init() {
this.createWindow();
this.createTray();
this.createMenu();
this.registerIPC();
}
private createWindow() {
this.win = new BrowserWindow({
width: 1400,
height: 900,
titleBarStyle: 'hiddenInset', // macOS native
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
contextIsolation: true,
nodeIntegration: false,
spellcheck: false,
},
});
if (process.env.NODE_ENV === 'development') {
this.win.webContents.openDevTools({ mode: 'detach' });
}
this.win.loadURL('http://localhost:5173'); // Vite dev server
}
}
new Main();
2. Preload (Type-Safe IPC Bridge)
// src/preload/index.ts
import { contextBridge, ipcRenderer } from 'electron';
import type { ElectronAPI } from '../main/types';
const api: ElectronAPI = {
// File system
readFile: (path: string) => ipcRenderer.invoke('fs:read', path),
writeFile: (path: string, content: string) =>
ipcRenderer.invoke('fs:write', path, content),
// System
openExternal: (url: string) => ipcRenderer.invoke('shell:open', url),
minimize: () => ipcRenderer.invoke('window:minimize'),
quit: () => ipcRenderer.invoke('app:quit'),
// Events
onFileChange: (callback: (path: string, content: string) => void) =>
ipcRenderer.on('file:change', callback),
};
contextBridge.exposeInMainWorld('electron', api);
3. React Renderer (Tailwind + shadcn/ui)
// src/renderer/App.tsx
import { useState, useEffect } from 'react';
interface ElectronAPI {
readFile: (path: string) => Promise<string>;
writeFile: (path: string, content: string) => Promise<void>;
}
declare global {
interface Window { electron: ElectronAPI; }
}
export default function App() {
const [filePath, setFilePath] = useState('/etc/hosts');
const [content, setContent] = useState('');
const loadFile = async () => {
try {
const data = await window.electron.readFile(filePath);
setContent(data);
} catch (error) {
console.error('Failed to load file:', error);
}
};
const saveFile = async () => {
try {
await window.electron.writeFile(filePath, content);
alert('File saved!');
} catch (error) {
console.error('Failed to save file:', error);
}
};
return (
<div className="h-screen bg-linear-to-br from-slate-50 to-blue-50 p-6">
<div className="max-w-6xl mx-auto h-full flex flex-col bg-white/70 backdrop-blur-xl rounded-3xl shadow-2xl border border-white/50">
<div className="p-8 border-b border-slate-200">
<h1 className="text-4xl font-black bg-linear-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
Electron File Editor
</h1>
</div>
<div className="p-8 flex-1 flex gap-6">
<div className="w-80 space-y-4">
<input
value={filePath}
onChange={(e) => setFilePath(e.target.value)}
className="w-full p-3 border border-slate-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="File path..."
/>
<div className="space-y-2">
<button
onClick={loadFile}
className="w-full bg-blue-600 text-white py-3 px-6 rounded-xl font-semibold hover:bg-blue-700 shadow-lg transition-all"
>
π Load File
</button>
<button
onClick={saveFile}
className="w-full bg-green-600 text-white py-3 px-6 rounded-xl font-semibold hover:bg-green-700 shadow-lg transition-all"
>
πΎ Save File
</button>
<button
onClick={() => window.electron.minimize()}
className="w-full bg-slate-600 text-white py-3 px-6 rounded-xl font-semibold hover:bg-slate-700"
>
β Minimize
</button>
</div>
</div>
<div className="flex-1">
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
className="w-full h-full p-6 font-mono text-sm border border-slate-200 rounded-2xl resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent shadow-inner"
placeholder="File content..."
/>
</div>
</div>
</div>
</div>
);
}
π IPC Handlers (Main Process)
// src/main/ipc.ts
import { ipcMain } from 'electron';
import * as fs from 'fs/promises';
import * as path from 'path';
ipcMain.handle('fs:read', async (_event, filePath: string) => {
try {
const fullPath = path.resolve(filePath);
const content = await fs.readFile(fullPath, 'utf8');
return content;
} catch (error) {
throw new Error(`Failed to read ${filePath}: ${error}`);
}
});
ipcMain.handle('fs:write', async (_event, filePath: string, content: string) => {
try {
const fullPath = path.resolve(filePath);
await fs.writeFile(fullPath, content, 'utf8');
} catch (error) {
throw new Error(`Failed to write ${filePath}: ${error}`);
}
});
ipcMain.handle('window:minimize', () => {
const win = BrowserWindow.getFocusedWindow();
win?.minimize();
});
π½οΈ Native Menus + System Tray
// Native macOS/Windows menus
const menuTemplate: Electron.MenuItemConstructorOptions[] = [
{
label: 'File',
submenu: [
{ label: 'New', accelerator: 'CmdOrCtrl+N', click: () => {} },
{ label: 'Open', accelerator: 'CmdOrCtrl+O', click: () => {} },
{ type: 'separator' },
{ label: 'Quit', accelerator: 'CmdOrCtrl+Q', role: 'quit' },
],
},
{
label: 'View',
submenu: [
{ label: 'Toggle Developer Tools', accelerator: 'F12', click: () => win?.webContents.toggleDevTools() },
{ type: 'separator' },
{ role: 'resetzoom' },
{ role: 'zoomin' },
{ role: 'zoomout' },
],
},
];
Menu.setApplicationMenu(Menu.buildFromTemplate(menuTemplate));
// System tray icon
const tray = new Tray(path.join(__dirname, '../../assets/tray-icon.png'));
const trayMenu = Menu.buildFromTemplate([
{ label: 'Show App', click: () => win?.show() },
{ label: 'Quit', role: 'quit' },
]);
tray.setContextMenu(trayMenu);
tray.setToolTip('My Electron App');
π Auto-Updates (Production)
// electron-updater
import { autoUpdater } from 'electron-updater';
import { dialog } from 'electron';
app.whenReady().then(() => {
autoUpdater.checkForUpdatesAndNotify();
});
autoUpdater.on('update-available', () => {
dialog.showMessageBox({
type: 'info',
title: 'Update Available',
message: 'A new version is available. Restart to update.',
buttons: ['Restart', 'Later'],
}).then(({ response }) => {
if (response === 0) autoUpdater.quitAndInstall();
});
});
π¦ Production Packaging (electron-builder)
// electron-builder.config.ts
import type { Configuration } from 'electron-builder';
const config: Configuration = {
appId: 'com.example.myapp',
productName: 'My Desktop App',
directories: { output: 'dist' },
files: [
'dist/**/*',
'node_modules/**/*',
'assets/**/*',
'!**/*.map',
],
mac: {
category: 'public.app-category.productivity',
target: ['dmg', 'zip'],
icon: 'assets/icon.icns',
hardenedRuntime: true,
gatekeeperAssess: false,
entitlements: 'assets/entitlements.mac.plist',
notarize: { teamId: process.env.APPLE_TEAM_ID! },
},
win: {
target: [
{
target: 'nsis',
arch: ['x64', 'arm64'],
},
],
icon: 'assets/icon.ico',
requestedExecutionLevel: 'asInvoker',
},
linux: {
target: ['AppImage', 'deb', 'rpm'],
icon: 'assets/icon.png',
category: 'Utility',
},
publish: [
{
provider: 'github',
owner: process.env.GITHUB_OWNER!,
repo: process.env.GITHUB_REPO!,
private: false,
},
],
};
export default config;
π¨ Modern Stack Template
npm create electron-vite@latest my-app -- --template=react-ts-tailwind
# or
npm create electron-vite@latest my-app -- --template=vue-ts
npm create electron-vite@latest my-app -- --template=svelte-ts
Production Stack: β Electron 39 (Chromium 142, Node 22.20) β React 19 / Vue 3.5 / Svelte 5 β Vite 6 (HMR + 60fps) β Tailwind CSS v4 / shadcn/ui β TypeScript 5.6 β Zustand / Jotai state β electron-updater auto-updates β Sentry crash reporting
π Performance Benchmarks
| Metric | Electron v39 | Tauri v2 | Flutter Desktop |
|---|---|---|---|
| Cold Start | 1.2s | 0.3s | 0.8s |
| Memory (idle) | 120MB | 12MB | 45MB |
| Build Time | 45s | 20s | 60s |
| Hot Reload | 25ms | 50ms | 100ms |
π― Production Deployment
# Development
npm run dev # Hot reload + DevTools
# Production
npm run build # Renderer bundle
npm run make # Native packages (DMG/EXE/AppImage)
# Auto-publish
npm run make -- --publish=always
π― Production Checklist
β [] Electron 39 (Chromium 142, Node 22) β [] TypeScript + contextIsolation β [] Preload IPC bridge (type-safe) β [] Native menus + system tray β [] Auto-updates (electron-updater) β [] Code signing (macOS notarization) β [] Crash reporting (Sentry) β [] electron-builder multi-platform β [] GitHub releases + auto-publish β [] Vite HMR (60fps dev)
2026 Strategy: Electron β Productivity/Tools (75%) Tauri β Size-critical (20%) Flutter β Design-heavy (5%)
Examples: VS Code (500MB), Discord (200MB), Slack (180MB), Figma (250MB).
Build production desktop apps with Electronβs mature ecosystem π.
Electron: electronjs.org | Docs: electronjs.org/docs