Guide
Electron fundamentals explained
VS Code, Slack, Discord, and Figma all share the same foundation:
Electron wraps your HTML, CSS, and JavaScript inside a
dedicated Chromium window with full Node.js
access on the main side. You reuse the React or Vue skills from
Vite
projects, ship one codebase to Windows, macOS, and Linux, and reach deep
into the OS for filesystem, tray icons, and native menus. The cost is
installer size and RAM — trade-offs our
Tauri guide
covers from the lightweight side. This guide explains Electron’s
process model, the preload security bridge, IPC patterns,
electron-builder packaging, auto-update, a Harbor Fleet ops
dashboard worked example, a desktop framework decision table, common
pitfalls, and a production checklist.
Architecture: main process, renderer, and Chromium
Electron is not a single JavaScript runtime. It is a small native
shell that boots one main process (Node.js + Electron
APIs) and spawns one or more renderer processes (each
a Chromium tab showing your UI). The main process owns the application
lifecycle: it creates BrowserWindow instances, registers
global shortcuts, talks to the OS tray, and coordinates updates.
Each renderer loads your frontend — typically a
index.html bundle from Vite or webpack — and runs
in an isolated browser context.
Because Electron ships its own Chromium build, CSS and JavaScript behave identically on every platform. You are not debugging three different webview engines. You are shipping ~80–150 MB of browser runtime and paying hundreds of megabytes of idle RAM for apps that keep windows open in the background. For developer tools, chat clients, and design surfaces where consistency and Node ecosystem depth matter more than disk footprint, that trade-off is well understood.
What lives where
- Main process —
app,BrowserWindow,ipcMain,dialog,shell, native modules, auto-updater, background workers. - Renderer process — DOM, React/Vue components, canvas, WebGL; ideally no direct Node access.
- Preload script — runs in the renderer before your page loads; the only place that should bridge privileged APIs to the UI via
contextBridge.
Project structure and tooling
Modern Electron apps scaffold with Electron Forge, electron-vite, or a custom Vite + TypeScript template. A typical layout:
src/main/— main-process entry (main.ts), window creation, IPC handlers.src/preload/— preload script exposing a typed API to the renderer.src/renderer/— frontend SPA (React, Vue, Svelte, or plain HTML).electron-builder.ymlorforge.config.js— packaging targets, icons, code signing.
Development runs two servers in parallel: Vite serves the renderer on
localhost:5173 with hot module replacement, while Electron
loads that URL in dev mode. Production builds compile the renderer to
static assets and point loadFile at the output directory.
TypeScript projects often share types between main, preload, and
renderer through a src/shared/ package for IPC contracts.
IPC: preload scripts and contextBridge
Early Electron tutorials enabled nodeIntegration: true in
renderers, which let any XSS execute arbitrary filesystem commands.
Modern Electron defaults to context isolation and
sandboxed renderers. The renderer cannot call
require('fs') directly. Instead, the preload script
runs in an isolated world with limited Node access and publishes a
narrow API:
// preload.ts
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('fleet', {
readLogTail: (path: string, lines: number) =>
ipcRenderer.invoke('log:tail', path, lines),
onAlert: (cb: (msg: string) => void) => {
ipcRenderer.on('alert:new', (_e, msg) => cb(msg));
},
});
The main process registers matching handlers with
ipcMain.handle for request/response flows or
ipcMain.on for fire-and-forget events. Use
invoke/handle for async work (file reads,
database queries) and webContents.send from main to push
real-time updates (download progress, websocket alerts) into the UI.
IPC design rules
- Validate every argument in the main process; never trust renderer input for paths or shell commands.
- Expose verbs, not objects —
saveReport(json)beats handing renderers a rawfsmodule. - Keep payloads JSON-serializable; pass file paths as strings, not open handles.
- Remove listeners on window close to avoid memory leaks from orphaned callbacks.
Security defaults that matter
Electron’s threat model assumes untrusted content can load inside a renderer — remote images, user-supplied HTML in a preview pane, or a compromised npm dependency. Harden every window:
nodeIntegration: falseandcontextIsolation: trueon allwebPreferences.sandbox: truefor renderers that do not need preload Node APIs beyondcontextBridge.webSecurity: trueunless you have a documented reason to disable CORS in a dev-only tool.- Never load remote
http://content in production; usehttps://with certificate pinning or bundle assets locally. - Disable
allowRunningInsecureContentand navigation to arbitrary URLs viawill-navigatehandlers.
Treat the preload surface as a public API: version it, document breaking
changes, and audit exposed methods the way you would audit a REST
endpoint. One overly permissive exec(command) bridge
turns a CSS bug into remote code execution.
Packaging, distribution, and auto-update
electron-builder (or Forge’s makers) produces
platform installers: .exe + NSIS on Windows,
.dmg on macOS, .AppImage or .deb
on Linux. Configuration covers app ID, icons per density, file
associations, and ASAR archiving (packaging JavaScript into a read-only
archive that slightly speeds load and obscures source).
Code signing is non-negotiable for mainstream distribution. macOS requires Apple Developer ID signing plus notarization; Windows needs an Authenticode certificate to avoid SmartScreen warnings. CI pipelines on GitHub Actions typically build all three targets from tagged releases, upload artifacts, and attach them to a GitHub Release feed.
electron-updater polls that release feed (or S3) and downloads deltas in the background. Wire it in the main process only; notify renderers through IPC so the UI can prompt “Restart to update.” Pin update channels (stable, beta) and sign update manifests to prevent supply-chain substitution.
Native modules and performance
Electron ships its own Node ABI, so native addons compiled for system
Node will not load. Rebuild them with electron-rebuild or
@electron/rebuild against your Electron version. Common
uses: SQLite via better-sqlite3, image processing with
sharp, hardware key stores, and serial-port bridges for
IoT dashboards.
CPU-heavy work belongs off the UI thread. Spawn
utilityProcess (Electron 22+) or hidden
BrowserWindow workers for PDF rendering, log parsing, or
cryptography. The main process should never synchronously read large
files on the critical path to window creation — users perceive
startup time as product quality.
Worked example: Harbor Fleet ops dashboard
Harbor Fleet runs a desktop ops dashboard for warehouse supervisors who
need tray-resident alerts when conveyor telemetry crosses thresholds.
The team chose Electron because they already had a React +
TypeScript
monitoring UI and needed better-sqlite3 for offline shift
logs when the LAN drops.
Main process responsibilities
- On
app.whenReady, create a 1280×800 window, set a dock/tray icon, and register globalCtrl+Shift+Hto focus the window. - Start a background poller that tails local JSONL log files written by edge agents; push summarized alerts via
webContents.send('alert:new', payload). - Handle
ipcMain.handle('log:tail')with path allowlisting to~/HarborFleet/logs/only. - On
before-quit, flush SQLite WAL and cancel in-flight downloads.
Renderer responsibilities
- React dashboard with sparkline charts (Canvas) and a sortable alert table.
- Calls
window.fleet.readLogTailthrough the preload bridge; never touchesfsdirectly. - Subscribes to
window.fleet.onAlertfor toast notifications rendered with the Notifications API where OS permissions allow.
Packaging produces a 142 MB Windows installer — acceptable for a 24/7 control-room machine. A sibling project later prototyped the same UI in Tauri for field laptops with tight RAM; both share the Vite renderer, differing only in the IPC bridge layer.
Desktop framework decision table
| Need | Prefer | Why |
|---|---|---|
| Identical Chromium everywhere, rich npm native modules | Electron | Mature ecosystem; VS Code-scale apps prove the model |
| Smallest installer, low idle RAM, Rust systems access | Tauri | OS webviews instead of bundled Chromium |
| Mobile + desktop from one web codebase | Capacitor / Ionic | WebView on iOS/Android; Electron or Tauri for desktop targets |
| Pure native macOS look, SwiftUI polish | Swift / AppKit | No webview compromises on Apple platforms |
| Internal CLI-first tool, minimal GUI | Terminal + optional web dashboard | Skip desktop shell entirely until UX demands it |
Common pitfalls
- Re-enabling nodeIntegration — copy-pasted Stack Overflow answers from 2018 recreate critical XSS-to-RCE paths.
- Blocking main on long I/O — synchronous
readFileSyncduring startup freezes the splash screen; use async handlers. - Native module ABI mismatch — upgrading Electron without rebuilding addons causes obscure
MODULE_VERSIONcrashes. - Memory leaks from detached windows — hidden windows and uncleared
ipcMainlisteners accumulate over days-long sessions. - Remote content in production — loading analytics or help docs from HTTP invites MITM script injection.
- Skipping macOS notarization — Gatekeeper blocks unsigned builds; support tickets follow.
- ASAR unpacking surprises — native binaries and writeable config must live in
extraResources, not inside ASAR.
Production checklist
- Scaffold with Electron Forge or electron-vite; TypeScript across main, preload, and renderer.
- Set secure
webPreferencesdefaults on everyBrowserWindow. - Expose a minimal, typed preload API via
contextBridge; validate all IPC inputs in main. - Share IPC channel names and payload types through a
shared/types module. - Rebuild native modules in CI with
@electron/rebuildpinned to your Electron version. - Configure electron-builder targets for win, mac, and linux; store signing secrets in CI vaults.
- Test release builds on each OS — dev-server URLs hide asset-path bugs.
- Wire electron-updater with signed manifests and a user-visible restart prompt.
- Measure cold-start time, idle RAM, and installer size; document trade-offs vs Tauri for stakeholders.
- Log main-process crashes to disk; integrate Sentry or similar with source maps uploaded per release.
Key takeaways
- Electron pairs a Node main process with Chromium renderer windows for cross-platform desktop apps.
- Preload + contextBridge is the secure IPC boundary; never expose raw Node to untrusted renderer code.
- electron-builder and signed auto-updates are part of the product, not an afterthought.
- Native addons and heavy work need rebuild discipline and background processes to keep the UI responsive.
- Choose Electron when Chromium consistency and npm depth beat installer size; reach for Tauri when they do not.
Related reading
- Tauri fundamentals explained — lightweight desktop apps with Rust core and OS webviews
- Vite fundamentals explained — the typical renderer build tool in Electron projects
- TypeScript fundamentals explained — shared typing across main, preload, and renderer
- Node.js fundamentals explained — the runtime powering Electron’s main process