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
| Aspekt | Main | Renderer |
|---|---|---|
| Anzahl pro App | Genau 1 | 1 oder mehr (pro BrowserWindow) |
| Runtime | Node.js | Chromium |
| Hat Zugriff auf | Node-APIs (fs, child_process, …), Electron-Main-APIs | Web-APIs (DOM, Canvas, fetch …) |
| Sandbox-Default | Nein | Ja (mit sandbox: true, seit v20) |
| Stirbt → App stirbt? | Ja | Nein (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
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
<!doctype html>
<html>
<head><title>My App</title></head>
<body>
<div id="app"></div>
<script type="module" src="./renderer.js"></script>
</body>
</html>// 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
BrowserWindowgeladen 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:
- 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.
- Stabilität — Crasht ein Renderer (z. B. wegen Speicher-Leak in einem Webview), bleibt die App am Leben. Multi-Process aus Chromium übernommen.
- 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.
// 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.
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
- Process Model — offizielle Architektur-Doku
- Multithreading — Worker und utilityProcess
- Chromium Multi-Process Architecture — Hintergrund aus Chromium