Jede Electron-App ist eine Multi-Prozess-Anwendung: ein Main-Prozess für System-Zugriffe und einer oder mehrere Renderer-Prozesse für die UI. Die strikte Trennung ist kein Zufall — sie ist die Sicherheits- und Stabilitäts-Architektur, die Chromium aus dem Browser geerbt hat. Hier wer was macht, wie sie kommunizieren und welche Fallen drinstecken.

Die zwei Welten

AspektMainRenderer
Anzahl pro AppGenau 11 oder mehr (pro BrowserWindow)
RuntimeNode.jsChromium
Hat Zugriff aufNode-APIs (fs, child_process, …), Electron-Main-APIsWeb-APIs (DOM, Canvas, fetch …)
Sandbox-DefaultNeinJa (mit sandbox: true, seit v20)
Stirbt → App stirbt?JaNein (Fenster geht verloren, App bleibt)

Eine Faustformel: Main = Backend, Renderer = Frontend — analog zu Web-Apps, nur lokal verbunden via IPC statt HTTP.

Was im Main passiert

JavaScript Typischer Main-Code
import { app, BrowserWindow, Menu, Tray, dialog } from 'electron';
import fs from 'node:fs/promises';
import path from 'node:path';

app.whenReady().then(() => {
    // Fenster erzeugen
    const win = new BrowserWindow({ width: 1200, height: 800 });
    win.loadFile('index.html');

    // System-Tray
    const tray = new Tray('icon.png');
    tray.setToolTip('My App');

    // Menü
    Menu.setApplicationMenu(Menu.buildFromTemplate([...]));

    // Datei lesen (Node-API direkt)
    fs.readFile(path.join(app.getPath('userData'), 'config.json'));
});

Der Main hat:

  • Fenster-Management: BrowserWindow, WebContentsView
  • System-Integration: Tray, Menü, Notifications, globale Shortcuts
  • Native APIs: Filesystem, Netzwerk, Child-Processes
  • App-Lebenszyklus: ready, before-quit, will-quit, activate

Der Main ist single-threaded wie jeder Node-Prozess. Lange synchrone Operationen blockieren die ganze App — daher CPU-intensives in Worker oder Child-Process auslagern.

Was im Renderer passiert

HTML Renderer = ganz normales Web
<!doctype html>
<html>
<head><title>My App</title></head>
<body>
    <div id="app"></div>
    <script type="module" src="./renderer.js"></script>
</body>
</html>
JavaScript renderer.js
// Web-APIs ganz normal
const data = await fetch('/api/users').then(r => r.json());
document.getElementById('app').textContent = data.name;

// Aber: kein direkter Zugriff auf fs, child_process etc.
// Für Main-Funktionen → IPC via window.api (per contextBridge)
const version = await window.api.getAppVersion();

Renderer haben:

  • DOM und Web-APIs — wie im Browser
  • Frontend-Frameworks — React, Vue, Svelte, alles möglich
  • Web-Worker — separate Threads im Renderer
  • Fenster-Inhalte — alles was als BrowserWindow geladen wird

Was sie nicht haben:

  • Node-APIs (außer per Bridge über Preload)
  • Direkten System-Zugriff
  • Andere Fenster steuern

Warum diese Trennung?

Drei Gründe:

  1. Sicherheit — Renderer kann kompromittierten Web-Code laden (XSS, bösartige Werbung). Hätte er Node-Zugriff, wäre die ganze Maschine offen. Sandbox + IPC schützt davor.
  2. Stabilität — Crasht ein Renderer (z. B. wegen Speicher-Leak in einem Webview), bleibt die App am Leben. Multi-Process aus Chromium übernommen.
  3. Performance — Renderer können auf eigenen CPU-Cores laufen, parallel zum Main. Bei vielen Fenstern messbar.

Wie sie kommunizieren

Ausschließlich über IPC. Es gibt keinen geteilten Speicher zwischen Main und Renderer.

JavaScript
// Main: Endpunkt registrieren
ipcMain.handle('files:read', async (_event, name) => {
    return await fs.readFile(name, 'utf-8');
});

// Preload: Bridge bauen
contextBridge.exposeInMainWorld('api', {
    readFile: (name) => ipcRenderer.invoke('files:read', name)
});

// Renderer: aufrufen
const content = await window.api.readFile('config.json');

Mehr im Artikel IPC Grundlagen.

Preload — die dritte Welt

Zwischen Main und Renderer gibt es einen dritten Akteur: das Preload-Skript. Es läuft im Renderer-Prozess, aber vor dem Web-Code, mit Zugriff auf eingeschränkte Node-APIs (im Sandbox-Modus nur electron-spezifische).

Das Preload exponiert kontrollierte Funktionen über contextBridge — der Renderer sieht nur, was du explizit freigibst.

JavaScript preload.js
import { contextBridge, ipcRenderer } from 'electron';

contextBridge.exposeInMainWorld('api', {
    readFile: (path) => ipcRenderer.invoke('files:read', path),
    getVersion: () => ipcRenderer.invoke('app:get-version')
});

Mehr im Artikel Renderer & Preload.

Interessantes

Renderer ist NICHT der Browser.

Renderer nutzt Chromium, hat aber Electron-spezifische Defaults: keine CORS-Restriktionen für file://-Inhalte, anderer User-Agent, optionaler Node-Zugriff. Wer Web-Reflexe blind übernimmt, wundert sich oft.

nodeIntegration: true hebelt die ganze Trennung aus.

Ältere Tutorials zeigen das oft. Modern ist nodeIntegration: false + contextIsolation: true + sandbox: true. Nur über contextBridge wird die Brücke gebaut. Mehr im Sicherheits-Kapitel.

Multi-Window-Apps haben mehrere Renderer.

Jedes BrowserWindow ist sein eigener Renderer-Prozess. Sie wissen voneinander nichts — Kommunikation muss über den Main geleitet werden (Renderer-A schickt an Main, Main forwarded an Renderer-B).

Main-Prozess blockieren = ganze App eingefroren.

Im Main einen synchronen 5-Sekunden-Loop laufen lassen → alle Fenster frozen, Menü reagiert nicht, Tray ignoriert Klicks. CPU-intensives via worker_threads oder utilityProcess auslagern.

process.type verrät, wo du gerade bist.

In Node-Code mit process.type prüfen: 'browser' = Main, 'renderer' = Renderer-Prozess, 'utility' = utilityProcess. Hilfreich für Shared-Code-Module, die in beiden Welten landen können.

BrowserView ist deprecated — WebContentsView ist der Nachfolger.

Wer mehrere Renderer in einem Fenster braucht (z. B. Browser-artige Tab-Apps): nicht mehr BrowserView, sondern WebContentsView (ab Electron 28+). Gleicher Use-Case, modernere API.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Grundlagen

Zur Übersicht