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

JavaScript main.js
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, webFrame
  • events-Modul aus Node
  • Kein direktes fs, kein child_process, kein path (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

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

contextBridge.exposeInMainWorld('api', {
    ping: () => ipcRenderer.invoke('app:ping'),
    getVersion: () => ipcRenderer.invoke('app:get-version')
});
JavaScript preload.js (ESM, ab Electron 28)
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:

JavaScript renderer.js
const version = await window.api.getVersion();
console.log(version);

Was im Preload NICHT funktioniert (Sandbox an)

JavaScript preload.js — kaputte Patterns
// 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.

JavaScript Stattdessen — IPC zum Main
// 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:

JavaScript
// 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.

JSON package.json
{
  "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:

JavaScript Alt: CommonJS
const { contextBridge, ipcRenderer } = require('electron');

Mit "type": "module" und Electron 28+:

JavaScript Modern: ESM
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.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Renderer & Preload

Zur Übersicht