Electron combines Chromium and Node.js so you can build full-featured desktop apps with HTML, CSS, and JavaScript. VS Code, Slack, and Discord already use it, and so can you! ⚡

What is Electron? 🤔

Imagine building a web app, but instead of running in the browser, it runs as a standalone desktop application on Windows, macOS, and Linux. That's exactly what Electron makes possible. Under the hood, Electron bundles a Chromium browser and a Node.js runtime into one package. Your app gets full access to web APIs and the file system, OS features, and native modules.

Originally developed by GitHub for the Atom editor, Electron has become the go-to framework for cross-platform desktop apps.

Electron | Build cross-platform desktop apps with JavaScript, HTML, and CSS
Build cross-platform desktop apps with JavaScript, HTML, and CSS. If you can build a website, you can build a desktop app.

How Does Electron Work Under the Hood? ⚙️

Electron is built on two core components:

  • Chromium, renders your UI (HTML/CSS/JS), just like in a browser
  • Node.js, gives you access to the file system, network, native modules, and more

Every Electron app consists of two types of processes:

Main Process 🌱

The Main Process is the "boss." It starts the app, creates windows, manages the app lifecycle, and has full Node.js access. There's exactly one Main Process per app.

Renderer Process 🎨

Each window (BrowserWindow) gets its own Renderer Process. It's basically a Chromium tab. This is where your frontend code runs. For security reasons, the renderer should have no direct Node.js access.

Setting Up Your First Electron App 🚀

Let's jump right in. Create a new project:

mkdir my-electron-app && cd my-electron-app
npm init -y
npm install electron --save-dev

Your package.json should look like this:

{
  "name": "my-electron-app",
  "version": "1.0.0",
  "main": "main.js",
  "scripts": {
    "start": "electron .",
    "build": "electron-builder"
  },
  "devDependencies": {
    "electron": "^33.0.0",
    "electron-builder": "^25.0.0"
  }
}

Now the main.js, the heart of your app:

const { app, BrowserWindow } = require('electron');
const path = require('path');

function createWindow() {
  const win = new BrowserWindow({
    width: 1200,
    height: 800,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      nodeIntegration: false
    }
  });

  win.loadFile('index.html');
}

app.whenReady().then(() => {
  createWindow();

  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) {
      createWindow();
    }
  });
});

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

Note the webPreferences: contextIsolation: true and nodeIntegration: false are critically important for security. More on that later.

Electron Documentation
Getting started guides, API references, and tutorials for building Electron apps.

Preload Scripts, The Secure Bridge 🛡️

The preload script is the secure intermediary between the Main and Renderer processes. It runs in the Renderer's context but has access to some Node.js APIs:

// preload.js
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
  // Secure API for the Renderer
  getSystemInfo: () => ipcRenderer.invoke('get-system-info'),
  openFile: () => ipcRenderer.invoke('dialog:openFile'),
  onUpdateAvailable: (callback) => {
    ipcRenderer.on('update-available', (_event, info) => callback(info));
  },
  saveData: (data) => ipcRenderer.invoke('save-data', data)
});

With contextBridge.exposeInMainWorld, you only expose the functions the Renderer actually needs. No direct Node.js access, no security holes.

IPC Communication, Main & Renderer Talking to Each Other 💬

IPC (Inter-Process Communication) is the backbone of every Electron app. Here's how Main and Renderer processes communicate securely:

// main.js - Register IPC handlers
const { ipcMain, dialog } = require('electron');
const os = require('os');
const fs = require('fs');

ipcMain.handle('get-system-info', () => {
  return {
    platform: os.platform(),
    arch: os.arch(),
    cpus: os.cpus().length,
    totalMemory: Math.round(os.totalmem() / 1024 / 1024 / 1024) + ' GB',
    nodeVersion: process.versions.node,
    electronVersion: process.versions.electron
  };
});

ipcMain.handle('dialog:openFile', async () => {
  const { canceled, filePaths } = await dialog.showOpenDialog({
    properties: ['openFile'],
    filters: [{ name: 'Text Files', extensions: ['txt', 'md', 'json'] }]
  });
  if (canceled) return null;
  return fs.readFileSync(filePaths[0], 'utf-8');
});

ipcMain.handle('save-data', async (_event, data) => {
  const { canceled, filePath } = await dialog.showSaveDialog({
    filters: [{ name: 'JSON', extensions: ['json'] }]
  });
  if (canceled || !filePath) return false;
  fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
  return true;
});

And in the Renderer, you use the exposed API:

// renderer.js
async function showSystemInfo() {
  const info = await window.electronAPI.getSystemInfo();

  const container = document.getElementById('system-info');
  // Clear existing content
  while (container.firstChild) {
    container.removeChild(container.firstChild);
  }

  Object.entries(info).forEach(([key, value]) => {
    const p = document.createElement('p');
    const strong = document.createElement('strong');
    strong.textContent = key + ': ';
    p.appendChild(strong);
    p.appendChild(document.createTextNode(String(value)));
    container.appendChild(p);
  });
}

document.getElementById('btn-sysinfo')
  .addEventListener('click', showSystemInfo);

document.getElementById('btn-open')
  .addEventListener('click', async () => {
    const content = await window.electronAPI.openFile();
    if (content) {
      document.getElementById('file-content').textContent = content;
    }
  });

Using TypeScript 💪

Electron and TypeScript are a dream team. TypeScript gives you autocompletion, type checking, and makes your code significantly more robust. Especially with IPC communication, where you're sending data between processes, types are worth their weight in gold.

Warum du nur noch TypeScript nutzen solltest ☝️
TypeScript bringt Typsicherheit und bessere Entwicklererfahrung in deine JavaScript-Projekte.
// types.ts - Shared types for Main and Renderer
interface SystemInfo {
  platform: string;
  arch: string;
  cpus: number;
  totalMemory: string;
  nodeVersion: string;
  electronVersion: string;
}

interface ElectronAPI {
  getSystemInfo: () => Promise<SystemInfo>;
  openFile: () => Promise<string | null>;
  saveData: (data: unknown) => Promise<boolean>;
  onUpdateAvailable: (callback: (info: UpdateInfo) => void) => void;
}

declare global {
  interface Window {
    electronAPI: ElectronAPI;
  }
}

Packaging with electron-builder 📦

When your app is ready, you'll want to distribute it as an installable application. electron-builder is the standard tool for this:

{
  "build": {
    "appId": "com.example.myapp",
    "productName": "My Electron App",
    "directories": {
      "output": "dist"
    },
    "win": {
      "target": ["nsis", "portable"],
      "icon": "assets/icon.ico"
    },
    "mac": {
      "target": ["dmg", "zip"],
      "icon": "assets/icon.icns",
      "category": "public.app-category.developer-tools"
    },
    "linux": {
      "target": ["AppImage", "deb"],
      "icon": "assets/icon.png",
      "category": "Development"
    },
    "publish": [
      {
        "provider": "github",
        "owner": "your-username",
        "repo": "your-repo"
      }
    ]
  }
}
# Build for the current platform
npm run build

# For all platforms (may require specific environment)
npx electron-builder --win --mac --linux

Auto-Updates 🔄

One of the coolest features of Electron is the ability to deliver automatic updates. With electron-updater (part of electron-builder), here's how:

// main.js - Auto-Update Setup
const { autoUpdater } = require('electron-updater');

app.whenReady().then(() => {
  createWindow();

  // Check for updates
  autoUpdater.checkForUpdatesAndNotify();
});

autoUpdater.on('update-available', (info) => {
  // Notify the Renderer
  mainWindow.webContents.send('update-available', info);
});

autoUpdater.on('update-downloaded', (info) => {
  // Install update and restart app
  autoUpdater.quitAndInstall();
});

For this to work, you need to host your releases on GitHub (or another provider). electron-builder takes care of the rest.

Security, What You Need to Know 🔒

Security is critical with Electron because your app has full system access. Here are the most important rules:

SettingRecommendationWhy?
nodeIntegrationfalsePrevents direct Node.js access in the Renderer
contextIsolationtrueIsolates preload script from web content
sandboxtrueAdditional process isolation
webSecuritytrueEnables Same-Origin Policy

Additional tips:

  • Validate IPC inputs, Never trust data from the Renderer
  • Don't load remote content without a Content Security Policy
  • Keep Electron updated, Chromium updates patch security vulnerabilities
  • Don't use shell.openExternal() with unvalidated URLs

Performance Tips ⚡

Electron apps have a reputation for being resource-hungry. With these tips, you'll get the most out of your app:

  • Lazy Loading, Load modules only when they're needed
  • Use BrowserWindow wisely, Close unneeded windows or use win.hide()
  • V8 Snapshots, Reduce startup time with v8-compile-cache
  • Disable DevTools in production
  • Background Throttling, backgroundThrottling: true for non-visible windows
  • Asar Packaging, Pack app files into an asar archive for faster loading
// Lazy Loading example
async function loadHeavyModule() {
  const { heavyFunction } = await import('./heavy-module.js');
  return heavyFunction();
}

// Instead of loading everything at startup
app.whenReady().then(async () => {
  createWindow();
  // Load heavy modules after the window
  setTimeout(async () => {
    const analytics = await import('./analytics.js');
    analytics.init();
  }, 2000);
});

Node.js Backend Integration 🖥️

Since Electron has Node.js built in, you can use backend logic directly in your app. However, if you need a proper backend server, for example, for an API or database connection, NestJS can be a great complement:

NestJS: Server-Framework auf Steroide 🎉
NestJS bringt Struktur und Architektur in deine Node.js-Backend-Entwicklung.

Reusable UI Components 🧱

A big advantage of Electron: you can share your UI components between web app and desktop app. Web Components are particularly well-suited here because they're framework-agnostic:

Web Components: Eigene HTML-Elemente ohne Framework 🧱
Web Components sind native Browser-APIs für eigene HTML-Elemente – Shadow DOM, Templates und Custom Elements ganz ohne Framework.

Electron vs. Tauri, The Comparison 🥊

Tauri is the up-and-coming challenger. Here's an honest comparison:

CriterionElectronTauri
Language (Backend)JavaScript/Node.jsRust
Rendering EngineChromium (bundled)System WebView
Bundle Size~150 MB+~5-10 MB
RAM UsageHigherLower
EcosystemHuge, matureGrowing
Learning Curve (Web Devs)LowMedium (Rust)
PlatformsWin/Mac/LinuxWin/Mac/Linux/Mobile
StabilityBattle-testedGood, but younger

My take: If you're coming from the web world and want to get started quickly, Electron is the safe choice. If bundle size and performance are critical and you're willing to learn Rust, Tauri is extremely exciting.

Real-World Examples 🌎

These apps use Electron and you probably use them daily:

  • VS Code, The most popular code editor out there
  • Slack, Team communication
  • Discord, Gaming & community chat
  • Obsidian, Notes and knowledge management
  • Figma Desktop, Design tool
  • Notion Desktop, Productivity
  • 1Password, Password manager

This shows: Electron is absolutely production-ready for apps of any size.

Conclusion 🎯

Electron remains the most powerful framework for cross-platform desktop apps when you're coming from the web world. Yes, bundle sizes are larger than native apps, and RAM usage is higher. But in return, you get:

  • The entire web ecosystem (npm, frameworks, tools)
  • A massive community and documentation
  • Battle-tested across millions of installations
  • Rapid development with familiar technologies

Give it a try, with the security and performance tips from this article, you're all set! 🚀