send und on sind die ältere, Event-basierte IPC-Variante. Anders als invoke/handle gibt es keinen direkten Return-Value — was sie für Fire-and-Forget und vor allem für Push-Events vom Main an den Renderer ideal macht. Plus eine wichtige Warnung: sendSync ist quasi nie die richtige Wahl.
Renderer → Main (Fire-and-Forget)
// Preload exposed:
contextBridge.exposeInMainWorld('analytics', {
track: (event, payload) => ipcRenderer.send('analytics:track', event, payload)
});
// Renderer:
window.analytics.track('button_click', { id: 'submit' });ipcMain.on('analytics:track', (_event, name, payload) => {
sendToAnalyticsBackend(name, payload);
// Kein Return-Wert — Renderer hat schon weitergemacht
});send returned sofort. Der Renderer bekommt keine Bestätigung, kein Resultat. Für Telemetrie, Logging, „Status-Update an den Main" perfekt.
Main → Renderer (Push-Events)
Andersherum — vom Main an den Renderer pushen — ist DIE Hauptanwendung von send/on:
// Beim Download-Progress an Renderer pushen
downloader.on('progress', (percent) => {
mainWindow.webContents.send('download:progress', { percent });
});
// An ALLE Renderer broadcasten
nativeTheme.on('updated', () => {
BrowserWindow.getAllWindows().forEach(win => {
win.webContents.send('theme:changed', nativeTheme.shouldUseDarkColors);
});
});webContents.send(channel, ...args) ist das Pendant zu ipcRenderer.send. Der Renderer hört mit ipcRenderer.on (über Preload).
contextBridge.exposeInMainWorld('api', {
onDownloadProgress: (callback) => {
const listener = (_event, data) => callback(data);
ipcRenderer.on('download:progress', listener);
return () => ipcRenderer.removeListener('download:progress', listener);
}
});const unsubscribe = window.api.onDownloadProgress(({ percent }) => {
updateProgressBar(percent);
});
// Cleanup beim Unmount
unsubscribe();Antwort über zweiten Channel
Wer mit send/on doch eine Antwort braucht — älteres Pattern (heute idR durch invoke/handle ersetzt):
ipcMain.on('files:read-request', async (event, path) => {
const content = await fs.readFile(path, 'utf-8');
event.sender.send('files:read-response', { path, content });
});ipcRenderer.send('files:read-request', '/path/to/file');
ipcRenderer.once('files:read-response', (_event, { content }) => {
console.log(content);
});Funktioniert, ist aber umständlich — invoke/handle macht das in einer Zeile. Heute nur noch in Legacy-Code zu sehen.
sendSync — fast immer falsch
// Synchroner Call — BLOCKIERT den Renderer
const result = ipcRenderer.sendSync('app:get-version');ipcMain.on('app:get-version', (event) => {
event.returnValue = app.getVersion();
});Während der Main antwortet, friert der Renderer ein — UI reagiert nicht, Animationen stoppen. Bei langsamen Operationen merkt der User das sofort.
Pragmatisch:
- Niemals
sendSyncfür Operationen, die mehr als ein paar Mikrosekunden brauchen - Sehr selten für super-schnelle Lookups beim App-Start (z. B. initiale Locale)
- Idiomatisch: immer
invokenutzen
Mehrere Listener pro Channel
Im Gegensatz zu handle darf ipcMain.on mehrere Listener pro Channel haben:
ipcMain.on('app:event', (_event, payload) => {
console.log('Listener A:', payload);
});
ipcMain.on('app:event', (_event, payload) => {
saveToDatabase(payload);
});
// Beide werden bei jedem send gerufenPraktisch für Cross-Cutting-Concerns wie Logging — ein Logger-Listener neben den eigentlichen Business-Listenern.
once für einmalige Events
ipcMain.once('app:initial-config', (_event, config) => {
applyConfig(config);
// Nach diesem ersten Aufruf wird der Listener entfernt
});
// Renderer
ipcRenderer.once('app:welcome-shown', () => {
console.log('Wurde gezeigt');
});once triggert nur beim ersten Match und entfernt sich dann selbst. Praktisch für Setup-Sequenzen.
FAQ
Wann send/on statt invoke/handle?
Wann immer keine Antwort gebraucht wird — Telemetrie, Logging, Push-Events vom Main. Für Renderer-fragt-Main-mit-Antwort: invoke. Pragmatische Faustregel: webContents.send für Push, invoke für Pull.
Sollte ich sendSync nutzen?
Praktisch nie. Es blockiert den Renderer bis zur Antwort — bei selbst kurzen Operationen UX-störend. Wer denkt „muss synchron sein": fast immer ist es das nicht. Stattdessen invoke mit await.
Wie warte ich auf eine Antwort mit send/on?
Pattern: send plus once-Listener auf einem Reply-Channel. Aber das ist genau das, was invoke/handle einfacher macht — modern fast immer invoke nehmen.
Werden Events bei abgemeldeten Listenern verloren?
Ja — wenn beim webContents.send kein Listener im Renderer aktiv ist, wird das Event verworfen. Pattern: vor dem ersten Push sicherstellen, dass der Renderer ready ist (z. B. über initialen did-finish-load).
Wie broadcaste ich an alle Fenster?
BrowserWindow.getAllWindows().forEach(win => win.webContents.send(channel, data)). Beachten: nur die, deren Renderer auch bereit sind. Wer kompliziertere Broadcasts braucht: eigenen kleinen Pub/Sub im Main bauen.
Cleanup-Pattern für mehrfach instanziierte Komponenten?
Listener bei jeder componentDidMount/onMount registrieren, bei componentWillUnmount/onDestroy wieder ab. Pattern: Subscription-Funktion gibt Unsubscribe-Funktion zurück, die im Cleanup aufgerufen wird.