Das Preload-Skript ist der dritte Akteur neben Main und Renderer — ein kleines Stück Code, das im Renderer-Kontext läuft, aber vor dem Web-Code geladen wird und Zugriff auf eingeschränkte Node-APIs hat. Ohne Preload bekommt der Renderer keine Brücke zum Main; mit ihm bekommst du eine kontrollierte, sichere API ins window-Objekt.
Wo das Preload sitzt
import { app, BrowserWindow } from 'electron';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
app.whenReady().then(() => {
const win = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
sandbox: true,
nodeIntegration: false
}
});
win.loadFile('index.html');
});preload ist eine Property von webPreferences — pro BrowserWindow wählbar. Mehrere Fenster können unterschiedliche Preloads haben.
Der Pfad muss absolut sein. path.join(__dirname, 'preload.js') ist der idiomatische Weg.
Was das Preload kann
Im Preload-Kontext (mit sandbox: true):
electron-Modul:contextBridge,ipcRenderer,webFrameevents-Modul aus Node- Kein direktes
fs, keinchild_process, keinpath(im Sandbox) process.platform,process.versions— read-only- DOM-Globals (
window,document) sind noch nicht voll verfügbar
Ohne Sandbox (sandbox: false):
- Alle Node-APIs verfügbar — mächtiger, aber Sicherheits-Risiko
- Empfehlung: Sandbox an lassen, was nötig ist im Main lassen
Minimal-Preload mit contextBridge
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('api', {
ping: () => ipcRenderer.invoke('app:ping'),
getVersion: () => ipcRenderer.invoke('app:get-version')
});import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('api', {
ping: () => ipcRenderer.invoke('app:ping'),
getVersion: () => ipcRenderer.invoke('app:get-version')
});exposeInMainWorld('api', { ... }) macht das Objekt unter window.api im Renderer verfügbar. Im Renderer kannst du dann:
const version = await window.api.getVersion();
console.log(version);Was im Preload NICHT funktioniert (Sandbox an)
// FEHLER: fs nicht verfügbar im Sandbox-Preload
const fs = require('node:fs');
// FEHLER: kein child_process
const { exec } = require('node:child_process');
// FEHLER: kein direktes path-Modul
const path = require('node:path');Wer das braucht: über IPC delegieren. Der Main hat den vollen Node-Zugriff.
// preload.js
contextBridge.exposeInMainWorld('files', {
read: (p) => ipcRenderer.invoke('files:read', p),
write: (p, content) => ipcRenderer.invoke('files:write', p, content)
});
// main.js
ipcMain.handle('files:read', (_e, p) => fs.readFile(p, 'utf-8'));
ipcMain.handle('files:write', (_e, p, content) => fs.writeFile(p, content));Mehrere Preloads pro App
Pro Fenster ein eigenes Preload möglich — nützlich für unterschiedliche Vertrauens-Stufen:
// Hauptfenster: volles API
const main = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload-main.js')
}
});
// Externes Embed (z. B. eingebettete Webseite): minimaleres API
const embed = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload-embed.js'),
sandbox: true
}
});Wenn du zum Beispiel eine externe URL lädst (Werbung, OAuth-Provider), willst du dort nicht das volle App-API exposen — eingeschränktes Preload reicht.
ESM in Preload-Skripten
Seit Electron 28 sind ESM-Preloads stabil unterstützt — vorher waren sie CommonJS-only.
{
"type": "module"
}Mit "type": "module" werden .js-Dateien als ESM gelesen. Wer einzelne CJS-Files braucht: .cjs-Endung.
Vor Electron 28 sah Preload-Code aus:
const { contextBridge, ipcRenderer } = require('electron');Mit "type": "module" und Electron 28+:
import { contextBridge, ipcRenderer } from 'electron';Wichtig: das Preload muss die gleiche Konvention nutzen wie package.json type — sonst kommt ein Loader-Error.
Besonderheiten
Preload läuft VOR dem Renderer-Code.
Wenn die index.html lädt, wird zuerst das Preload ausgeführt, dann erst die Web-Skripte. Das ist der einzige Zeitpunkt, an dem das Preload window modifizieren kann, bevor Web-Code es sieht.
contextBridge ist die einzige sichere Bridge.
window.foo = ... direkt im Preload (mit contextIsolation: true) hat keinen Effekt — die beiden Welten sind isoliert. Nur contextBridge.exposeInMainWorld(...) überquert die Grenze sicher.
Sandbox-Preload ist der Default seit v20.
Sandbox an, kein direktes fs/child_process. Das ist gewollt — die Sicherheits-Logik baut darauf. Wer eine Library braucht, die im Preload nicht läuft: ins Main-Modul mitnehmen und über IPC bedienen.
ESM-Preload braucht type: module in package.json.
Ab Electron 28. Davor: CJS mit require. In bestehenden Projekten lohnt der Wechsel — moderne Syntax, gleiches Verhalten.
Preload kann globale Listener registrieren.
Du kannst im Preload schon window.addEventListener('DOMContentLoaded', ...) setzen — feuert genau einmal, wenn das HTML geladen ist. Praktisch für Setup-Code, der keine Web-DOM-Library braucht.
Preload-Datei NICHT bundlen wie Renderer-Code.
Wenn du Vite/Webpack nutzt: Preload braucht ein eigenes Bundling — typisch ESBuild oder eine separate Vite-Konfig. electron-vite macht das automatisch. Bei plain npm: Preload als separates Skript pflegen, kein Babel/Webpack-Pipeline drum herum.