Electron's Approach

How Electron enables building desktop applications with web technologies using Node.js and Chromium

Electron is an open-source framework that lets you build cross-platform desktop applications with web technologies: HTML, CSS, and JavaScript. Instead of learning platform-specific languages like C# for Windows or Swift for macOS, you can use the skills you already have as a web developer to create apps that run on Windows, macOS, and Linux. Electron achieves this by bundling a complete Chromium browser engine and a Node.js runtime with every application.

This means an Electron app is essentially a standalone web page that can interact with the operating system through Node.js APIs. Your user interface is just a browser window, and your backend logic runs in a Node.js process that has full access to the file system, system tray, native menus, and more.

Why Electron Exists

Before Electron, building a desktop application that worked on all major operating systems meant maintaining separate codebases or using frameworks with limited UI capabilities. Electron was released by GitHub in 2013 to solve a concrete problem: the Atom code editor needed a desktop shell that could render complex HTML-based editor interfaces while having deep access to the filesystem and OS. Rather than build a custom solution from scratch, GitHub extracted the core technology into Electron and open-sourced it.

The fundamental insight was that web rendering engines had become powerful enough to drive complex desktop interfaces. Browsers could handle layouts, animations, fonts, and interactivity at a level that rivaled native toolkits. By pairing Chromium’s rendering with Node.js’s system access, Electron turned every web developer into a potential desktop app developer.

Electron’s Architecture — The Two-Process Model

Every Electron application consists of exactly two types of processes: a single main process and one or more renderer processes.

The Main Process

The main process runs in a Node.js environment. It is responsible for managing the application lifecycle: creating browser windows, handling application-level events (like quitting or opening files), and interacting with native operating system APIs. This process does not render any UI directly.

To create a window, the main process uses Electron’s BrowserWindow module. Each window that opens becomes its own renderer process.

main.js
const { app, BrowserWindow } = require('electron');
function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: false,
      contextIsolation: true,
    },
  });
  win.loadFile('index.html');
}
app.whenReady().then(createWindow);

After the app module signals that it is ready, createWindow constructs a BrowserWindow and loads an HTML file. This is the entry point for the entire user interface. The options passed to BrowserWindow control security settings, window size, and more.

The main process can also register global shortcuts, create menus, and spawn child processes. Because it has full Node.js access, it can read from disk, write databases, and make network requests — all outside the renderer’s view.

The Renderer Process

Each renderer process is a Chromium browser tab. It loads an HTML file and runs the JavaScript and CSS inside it, just like a web page. By default, renderer processes cannot access Node.js APIs directly. Instead, they communicate with the main process through a technique called inter-process communication (IPC) (for a comparison with Tauri's IPC model, see Comparison Table and When to Choose Tauri vs Electron).

A renderer process is sandboxed: it cannot touch the file system or open native dialogs on its own. If the UI needs to save a file, it sends an IPC message to the main process, which performs the operation and sends back the result. This separation is not just architectural discipline — it is critical for security.

The ipcMain and ipcRenderer modules provide the messaging layer. The main process listens for requests, and the renderer sends them.

preload.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('api', {
  saveFile: (content) => ipcRenderer.invoke('save-file', content),
});

The preload script runs in a privileged context before the renderer loads. It exposes a controlled API to the renderer via contextBridge, so the frontend can call window.api.saveFile(...) without ever touching require or Node.js primitives directly.

nodeIntegration Is Dangerous:

Setting nodeIntegration: true in a renderer’s web preferences gives the web page full access to Node.js. If your app loads any remote content or has an XSS vulnerability, this can let an attacker execute arbitrary code on the user’s machine. Always disable nodeIntegration and use contextIsolation with a preload script.

Bundling Chromium and Node.js — The Size Tradeoff

Electron ships its own copy of Chromium and Node.js inside every application. This ensures that your app runs identically on every operating system, regardless of what browser or Node version the user has installed. A user on a five-year-old macOS version gets the same rendering engine as someone on the latest Windows 11.

The direct consequence is app size. Before writing a single line of custom code, an Electron application already weighs approximately 85 MB on disk. This includes the full Chromium binary, the Node.js runtime, and Electron’s own C++ wrapper layer. For an app with a few windows, the total installed size can easily reach 150–250 MB.

Why Bundling?:

Electron bundles its own runtime to guarantee a consistent environment. If it relied on the system’s browser, differences between Safari, Chrome, and Firefox would cause unpredictable UI behavior. Bundling makes the developer’s life easier at the cost of larger downloads and higher disk usage.

Writing an Electron App — A Complete Example

Here is the minimum set of files needed for an Electron app that shows a window with a button. Clicking the button sends a message to the main process, which responds with the current time.

File structure:

my-app/
├── package.json
├── main.js
├── preload.js
└── index.html

package.json specifies the entry point and dependencies.

package.json
{
  "name": "minimal-electron-app",
  "version": "1.0.0",
  "main": "main.js",
  "scripts": {
    "start": "electron ."
  },
  "devDependencies": {
    "electron": "^34.0.0"
  }
}

main.js creates the window and handles IPC.

main.js
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
function createWindow() {
  const win = new BrowserWindow({
    width: 400,
    height: 300,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      nodeIntegration: false,
    },
  });
  win.loadFile('index.html');
}
ipcMain.handle('get-current-time', () => {
  return new Date().toLocaleTimeString();
});
app.whenReady().then(createWindow);

preload.js exposes the IPC channel safely to the renderer.

preload.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
  getCurrentTime: () => ipcRenderer.invoke('get-current-time'),
});

index.html contains the UI and calls the exposed API.

index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Electron Clock</title>
</head>
<body>
  <h1>Current Time</h1>
  <button id="get-time">Show Time</button>
  <p id="display"></p>
  <script>
    document.getElementById('get-time').addEventListener('click', async () => {
      const time = await window.electronAPI.getCurrentTime();
      document.getElementById('display').textContent = time;
    });
  </script>
</body>
</html>

When you run npm start, a window appears with a button. Clicking it sends an IPC request to the main process, which responds with the current time, and the renderer updates the paragraph.

The key takeaway from this example is the strict boundary: the renderer never calls require, never accesses the file system, and never handles native operations directly. All system access goes through a controlled channel defined in the preload script. This pattern is the standard for modern, secure Electron apps.

How Beginners Should Think About Electron

Think of an Electron app as a website that lives in its own dedicated browser, but that browser has a hidden control room attached. The website (renderer) cannot do anything dangerous by itself — it has to ask the control room (main process) for permission. The control room can read files, show native dialog boxes, and talk to the operating system. The two talk through a secure intercom (IPC).

This mental model helps you avoid the most common mistake: treating the renderer as if it were a Node.js script with full system power. In reality, the renderer is as limited as any web page, and you must route all privileged operations through the main process.

Common Mistakes and Misconceptions

“Electron apps are just web wrappers, so they can’t do native things.”
False. Through the main process’s Node.js runtime, an Electron app can use native modules, spawn child processes, register system-wide hotkeys, and integrate deeply with the OS.

“Enabling nodeIntegration is fine for internal tools.”
Even internal tools can be exploited if they display user-generated content or load third-party scripts. An attacker who finds an XSS bug can use Node.js integration to install ransomware. Always disable nodeIntegration.

“More windows means more main processes.”
Only one main process exists per application. Each window creates a new renderer process, but they all share the same main process. Spawning dozens of windows can still cause high memory usage because each renderer has its own Chromium instance.

Memory Consumption Adds Up Quickly:

A single Electron window typically uses between 100 MB and 300 MB of memory at rest. Six windows can easily push past 400 MB. This is not a bug — it is the direct consequence of each window running its own rendering engine instance. For applications that open many windows, monitor memory carefully and consider reusing views within a single window.

When Electron’s Approach Excels

Electron’s bundled runtime ensures that your app looks and behaves identically on every platform. If you test a layout on your Windows machine, it will render exactly the same on a colleague’s macOS, down to the pixel. This consistency is enormously valuable for teams that cannot afford to debug WebView-specific quirks across Safari, Chrome, and Firefox on different operating systems.

The framework’s maturity also means an established ecosystem of tools, tutorials, and libraries. Electron Forge handles building and packaging. Electron Builder generates platform-specific installers. Thousands of npm packages integrate seamlessly, and the debugging experience uses the same Chrome DevTools that web developers already know.

Consistency You Can Rely On:

If your app looks correct in development, it will look correct everywhere you deploy it — provided you use the same Electron version. This is the core tradeoff: you pay with disk and memory for a guarantee that no other desktop webview framework can match today.

Real-World Usage

Electron powers some of the most widely used desktop applications in the world. Visual Studio Code, Slack, Discord, Figma, and Notion all rely on it. These teams chose Electron not because it is lightweight, but because it allowed them to ship complex, feature-rich desktop applications with the same web development workflows they already used for their browser-based products.

For a startup building an API client with rich editor-like interfaces, Electron provides a shortcut to a mature text editing ecosystem (Monaco, CodeMirror) that has been battle-tested in the browser. For an enterprise building an internal dashboard that needs to integrate deeply with the file system, Electron’s Node.js integration makes that straightforward without learning a new language.

The tradeoffs — larger bundle sizes and higher memory usage — are often invisible to end users who have abundant disk space and RAM. The consistent experience and development speed are what matter to these teams.

The Security Baseline

Electron apps have a reputation for being insecure, but that is almost always a result of configuration choices, not the framework itself. The modern recommended security baseline is:

  • nodeIntegration: false everywhere
  • contextIsolation: true enabled
  • A preload script that uses contextBridge to expose only the minimal API
  • sandbox: true where possible, to isolate renderer processes from the OS

When configured this way, an Electron renderer is effectively a standard Chromium sandbox with no default access to Node.js. Exploiting a renderer vulnerability yields nothing beyond what an attacker could achieve in a regular Chrome tab visiting that page.