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 processapp, 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.yml or forge.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 raw fs module.
  • 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: false and contextIsolation: true on all webPreferences.
  • sandbox: true for renderers that do not need preload Node APIs beyond contextBridge.
  • webSecurity: true unless you have a documented reason to disable CORS in a dev-only tool.
  • Never load remote http:// content in production; use https:// with certificate pinning or bundle assets locally.
  • Disable allowRunningInsecureContent and navigation to arbitrary URLs via will-navigate handlers.

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 global Ctrl+Shift+H to 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.readLogTail through the preload bridge; never touches fs directly.
  • Subscribes to window.fleet.onAlert for 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 readFileSync during startup freezes the splash screen; use async handlers.
  • Native module ABI mismatch — upgrading Electron without rebuilding addons causes obscure MODULE_VERSION crashes.
  • Memory leaks from detached windows — hidden windows and uncleared ipcMain listeners 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 webPreferences defaults on every BrowserWindow.
  • 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/rebuild pinned 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