Hintergrundaufgaben ohne UI-Blockade
Der Browser-Main-Thread ist eine geteilte Ressource: Animationen, Event-Handler und Rendering konkurrieren mit eurer Geschäftslogik. requestIdleCallback löst diesen Konflikt, indem es nicht-kritische Aufgaben in die Leerlaufphasen des Browsers verschiebt – ohne dass der Nutzer irgendetwas davon bemerkt.
Inhaltsverzeichnis
- 1. Das Main-Thread-Problem: Warum requestIdleCallback existiert
- 2. Wie requestIdleCallback funktioniert
- 3. Die Deadline-API: timeRemaining und didTimeout
- 4. Arbeit in Chunks aufteilen
- 5. Der timeout-Parameter: Garantierte Ausführung
- 6. cancelIdleCallback und Lifecycle-Management
- 7. Fallback für Safari und ältere Browser
- 8. requestIdleCallback vs. rAF vs. setTimeout vs. Web Worker
- 9. Praxisbeispiel: Lazy Analytics-Queue
- 10. Zusammenfassung
- 11. FAQ
1. Das Main-Thread-Problem: Warum requestIdleCallback existiert
Jeder JavaScript-Code, der im Browser läuft, konkurriert um denselben Main Thread. Animationen, Input-Event-Handler, Fetch-Callbacks, Rendering – all das läuft seriell auf einem einzigen Thread. Wenn eine Aufgabe zu lange dauert, blockiert sie alle nachfolgenden Aufgaben. Der Nutzer sieht eingefrorene Animationen, verzögerte Klick-Reaktionen und – im schlimmsten Fall – den „Page Unresponsive"-Dialog. Das Problem ist nicht, dass die Aufgaben zu rechenintensiv sind, sondern dass sie zum falschen Zeitpunkt ausgeführt werden.
requestIdleCallback löst dieses Problem, indem es dem Browser die Kontrolle über den Ausführungszeitpunkt gibt. Der Entwickler registriert eine Aufgabe, der Browser führt sie aus, wenn der Main Thread nichts Dringenderes zu tun hat. Das ist ein Paradigmenwechsel: Statt zu entscheiden, wann eine Aufgabe läuft, delegiert man diese Entscheidung an den Browser, der den Gesamtzustand des Systems kennt. Die eigentliche Innovation liegt in der Deadline-API, die dem Callback mitteilt, wie viel Zeit noch im aktuellen Idle-Fenster verbleibt.
2. Wie requestIdleCallback funktioniert
Nach jedem gerenderten Frame hat der Browser möglicherweise Zeit übrig, bevor der nächste Frame-Zyklus beginnt. Bei 60 Hz hat ein Frame-Zyklus 16,6 ms. Wenn das Rendering in 10 ms erledigt ist, bleiben 6,6 ms Idle-Zeit. requestIdleCallback feuert in dieser Idle-Phase. Wenn der Browser dauerhaft ausgelastet ist – etwa weil kontinuierlich Animationen laufen und Scroll-Events verarbeitet werden – wird requestIdleCallback möglicherweise nie oder sehr selten aufgerufen. Das ist korrektes Verhalten: Die Idle-API hat explizit die niedrigste Priorität.
Neben Idle-Phasen zwischen Frames gibt es auch längere Idle-Perioden, wenn der Nutzer keine Interaktion durchführt. In diesen Phasen kann requestIdleCallback bis zu 50 ms Ausführungszeit erhalten – das sogenannte „Long Idle"-Fenster. Der Browser begrenzt dieses Fenster bewusst, weil Aufgaben, die länger als 50 ms laufen, bereits messbar als UI-Verzögerung wahrnehmbar sind, selbst wenn keine sichtbare Animation aktiv ist. requestIdleCallback-Callbacks müssen daher kooperativ sein: Sie prüfen das verbleibende Budget und unterbrechen sich selbst.
3. Die Deadline-API: timeRemaining und didTimeout
Der Callback, den man an requestIdleCallback übergibt, erhält als Argument ein IdleDeadline-Objekt mit zwei Feldern: timeRemaining() und didTimeout. timeRemaining() gibt zurück, wie viele Millisekunden im aktuellen Idle-Fenster noch verbleiben. Diese Methode wird in einer Schleife aufgerufen, um zu prüfen, ob weitere Arbeit erledigt werden kann. Sobald timeRemaining() 0 zurückgibt, unterbricht man die Schleife und plant die restliche Arbeit mit einem neuen requestIdleCallback-Aufruf.
Das Feld didTimeout ist true, wenn der Callback nur deshalb ausgeführt wird, weil der optionale timeout-Parameter abgelaufen ist – nicht weil echte Idle-Zeit vorhanden ist. In diesem Fall sollte man schnell und mit Bedacht agieren: Minimalarbeit erledigen und sofort unterbrechen, um die UI nicht zu blockieren. didTimeout ist der Sicherheitsnetz-Mechanismus, der garantiert, dass wichtige Hintergrundaufgaben nicht auf unbestimmte Zeit verschoben werden, wenn der Browser dauerhaft ausgelastet ist.
// requestIdleCallback with proper Deadline API usage
const tasks = [
() => processAnalyticsBatch(1),
() => prefetchNextPageContent(),
() => buildSearchIndex(),
() => cleanupStaleCache(),
() => sendPendingBeacons(),
];
let taskIndex = 0;
function runIdleTasks(deadline) {
// Process tasks while idle budget allows
while (taskIndex < tasks.length) {
const remaining = deadline.timeRemaining();
// Stop if no time left and not forced by timeout
if (remaining <= 0 && !deadline.didTimeout) break;
const task = tasks[taskIndex];
taskIndex++;
try {
task();
} catch (err) {
console.error('[idle] Task failed:', err);
// Continue with next task — don't block queue on error
}
}
// Schedule remaining tasks in next idle window
if (taskIndex < tasks.length) {
requestIdleCallback(runIdleTasks, { timeout: 2000 });
}
}
// Start the idle task queue
requestIdleCallback(runIdleTasks, { timeout: 5000 });
4. Arbeit in Chunks aufteilen
Die wichtigste Disziplin beim Einsatz von requestIdleCallback ist das Aufteilen langer Aufgaben in kleine, unterbrechbare Einheiten. Eine Funktion, die 200 ms braucht, ist für rIC ungeeignet – selbst wenn sie während eines Idle-Fensters startet, überschreitet sie das 50-ms-Budget und blockiert den Main Thread. Die Lösung: die Arbeit in eine Queue von Micro-Tasks aufteilen, die einzeln in weniger als 5 ms abgeschlossen werden. Pro Idle-Callback-Aufruf werden so viele Tasks wie möglich erledigt, bis das Budget erschöpft ist.
Für Daten-Transformationen auf großen Arrays empfiehlt sich das Generator-Pattern: Ein Generator kann bei jedem yield pausiert werden, und requestIdleCallback ruft ihn frame-weise auf. Das ist elegant und vermeidet die Notwendigkeit, Fortschrittsstatus manuell in externen Variablen zu verwalten. Für UI-Aufgaben wie das progressive Laden von Bildern oder das verzögerte Hydratieren von Komponenten funktioniert ein Array von Callbacks als Task-Queue am besten – lesbar, debugbar und ohne Generator-Komplexität.
5. Der timeout-Parameter: Garantierte Ausführung
Ohne timeout-Option gibt requestIdleCallback keine Garantie, dass der Callback jemals aufgerufen wird. Auf einer Seite mit dauerhaften Animationen und intensiver Nutzerinteraktion könnte Idle-Zeit niemals entstehen. Für Aufgaben, die zwar nicht dringend sind, aber dennoch innerhalb eines bestimmten Zeitrahmens erledigt sein müssen, ist der timeout-Parameter die richtige Lösung: requestIdleCallback(fn, { timeout: 3000 }) garantiert, dass fn spätestens nach 3 Sekunden aufgerufen wird – auch wenn kein Idle-Fenster verfügbar war.
Die Wahl des richtigen Timeout-Werts ist kontextabhängig. Analytics-Events können 10–30 Sekunden warten. Cache-Cleanup kann bis zu mehreren Minuten warten oder ohne Timeout laufen. Prefetch-Operationen für wahrscheinlich nächste Seiten sollten innerhalb von 2–5 Sekunden starten, damit der Cache aufgewärmt ist, bevor der Nutzer navigiert. Ohne timeout ist requestIdleCallback besonders nützlich für echte Hintergrundaufgaben wie Indizierung oder Vorverarbeitung, die komplett opportunistisch ausgeführt werden sollen.
// Generator-based chunked processing with requestIdleCallback
function* processLargeDataset(data) {
for (let i = 0; i < data.length; i++) {
// Perform one unit of work per yield
yield transformRecord(data[i]);
}
}
function scheduleChunkedWork(generator, onComplete) {
const results = [];
function idleCallback(deadline) {
// Consume generator while there is idle budget
while (deadline.timeRemaining() > 1 || deadline.didTimeout) {
const { value, done } = generator.next();
if (done) {
onComplete(results);
return; // All work completed
}
if (value !== undefined) {
results.push(value);
}
}
// More work remains — reschedule in next idle window
requestIdleCallback(idleCallback, { timeout: 10000 });
}
requestIdleCallback(idleCallback, { timeout: 10000 });
}
// Usage: transform 50 000 records without blocking the UI
const gen = processLargeDataset(rawRecords);
scheduleChunkedWork(gen, (results) => {
console.log('[idle] Processing complete:', results.length, 'records');
});
6. cancelIdleCallback und Lifecycle-Management
requestIdleCallback gibt einen numerischen Handle zurück, der für cancelIdleCallback(handle) genutzt werden kann. Das ist besonders wichtig in Komponenten-Architekturen: Wenn eine React-Komponente oder ein Custom Element entfernt wird, während noch eine Idle-Aufgabe ausstehend ist, muss die Aufgabe abgebrochen werden. Andernfalls versucht der Callback, DOM-Elemente zu manipulieren, die nicht mehr existieren, oder hält Referenzen auf entfernte Objekte – ein klassischer Memory-Leak.
Das Lifecycle-Pattern für requestIdleCallback in modernem JavaScript folgt dem gleichen Schema wie bei anderen asynchronen Browser-APIs: Handle in einer Variablen außerhalb des Callbacks speichern, im Destroy/Unmount-Lifecycle aufräumen. Für React: Handle in useRef speichern, im useEffect-Cleanup cancelIdleCallback aufrufen. Für Web Components: Handle als Instanzeigenschaft, cancelIdleCallback im disconnectedCallback. Für manuelle Singleton-Klassen: Cancel-Methode als öffentliche API exponieren.
7. Fallback für Safari und ältere Browser
requestIdleCallback ist in Safari seit Jahren nicht implementiert – das ist der wichtigste Browser-Kompatibilitäts-Vorbehalt dieser API. Wer Web-Applikationen für alle modernen Browser entwickelt, braucht einen Fallback. Der einfachste Ansatz: eine Wrapper-Funktion, die prüft, ob requestIdleCallback verfügbar ist, und andernfalls auf setTimeout(fn, 0) zurückfällt. setTimeout(fn, 0) gibt keine Idle-Garantien, ist aber für die meisten nicht-kritischen Hintergrundaufgaben ausreichend.
Ein robusterer Polyfill emuliert die IdleDeadline-Schnittstelle: Er übergibt dem Callback ein synthetisches Deadline-Objekt mit timeRemaining(), das eine feste Restzeit (z.B. 50 ms) zurückgibt, und didTimeout: false. Das ermöglicht es, denselben Callback-Code auf allen Browsern zu verwenden, ohne separate Code-Pfade. Für Produktions-Applikationen empfiehlt sich das npm-Paket requestidlecallback oder die offizielle Googlechrom-Polyfill-Implementierung, die das Timing-Verhalten genauer nachahmt.
8. requestIdleCallback vs. rAF vs. setTimeout vs. Web Worker
Die Wahl des richtigen Scheduling-Mechanismus ist entscheidend für die Performance-Architektur einer Webanwendung. requestIdleCallback hat die niedrigste Priorität und ist für Aufgaben gedacht, die weder visuell noch funktional dringend sind. requestAnimationFrame hat eine mittlere Priorität und ist für Aufgaben gedacht, die im nächsten Frame-Render abgeschlossen sein müssen. setTimeout mit kurzen Delays feuert so bald wie möglich, aber ohne Garantien bezüglich des Render-Zyklus. Web Workers laufen vollständig off-thread und sind für rechenintensive Aufgaben ohne DOM-Zugriff die stärkste Lösung.
Der entscheidende Unterschied zwischen requestIdleCallback und Web Workers: rIC läuft auf dem Main Thread und hat Zugriff auf den DOM. Web Workers haben keinen DOM-Zugriff und kommunizieren über postMessage. Für Aufgaben, die DOM-Manipulationen erfordern (Lazy Hydration, Progressive Enhancement, DOM-Strukturierung), ist requestIdleCallback die richtige Wahl. Für reine Daten-Transformationen, Kryptographie, Bild-Verarbeitung oder Suche über große Datensätze sind Web Workers überlegen.
| API | Priorität | DOM-Zugriff | Typischer Einsatz |
|---|---|---|---|
| requestIdleCallback | Niedrigste | Ja | Analytics, Prefetch, Lazy Hydration |
| requestAnimationFrame | Vor Render | Ja | Animationen, Scroll-Synchronisierung |
| setTimeout(fn, 0) | Nächster Task | Ja | Deferred Execution, Microtask-Nachfolger |
| Web Worker | Off-Thread | Nein | Daten-Transformation, Kryptographie |
9. Praxisbeispiel: Lazy Analytics-Queue
Analytics-Tracking ist der Paradefall für requestIdleCallback: Die Events müssen erfasst werden, sind aber für die Nutzererfahrung völlig irrelevant. Statt jeden Analytics-Event sofort zu senden, was Netzwerklatenz auf den Main Thread bringt, werden Events in einer Queue gesammelt. Ein requestIdleCallback-basierter Queue-Consumer verarbeitet und sendet die Events in Idle-Phasen, batched und priorisiert. Das Ergebnis: Die Seite fühlt sich responsiver an, und Analytics-Daten werden trotzdem zuverlässig erfasst.
Das Muster lässt sich auf viele ähnliche Szenarien übertragen: Lazy-Loading von nicht-sichtbaren Bildern (Prefetch), das Aufwärmen eines lokalen Suchindex, das Speichern von Draft-States in IndexedDB, das Vorkompilieren von Templates oder das Bereinigen veralteter Cache-Einträge. Allen diesen Aufgaben ist gemeinsam: Sie sind wichtig für die langfristige Benutzererfahrung, aber nicht für die unmittelbare Interaktion. requestIdleCallback ist für genau diese Kategorie von Aufgaben das richtige Werkzeug.
// Lazy analytics queue using requestIdleCallback
class AnalyticsQueue {
constructor() {
this.queue = [];
this.idleCallbackId = null;
this.endpoint = '/api/analytics/batch';
}
track(event, data) {
this.queue.push({ event, data, timestamp: Date.now() });
// Schedule flush if not already scheduled
if (this.idleCallbackId === null) {
this.idleCallbackId = requestIdleCallback(
this.flush.bind(this),
{ timeout: 10000 } // Guarantee flush within 10s
);
}
}
flush(deadline) {
this.idleCallbackId = null;
const batch = [];
// Drain queue within idle budget
while (this.queue.length > 0 && (deadline.timeRemaining() > 2 || deadline.didTimeout)) {
batch.push(this.queue.shift());
}
if (batch.length > 0) {
// Use sendBeacon for reliable delivery (non-blocking)
navigator.sendBeacon(this.endpoint, JSON.stringify(batch));
}
// More events in queue — reschedule
if (this.queue.length > 0) {
this.idleCallbackId = requestIdleCallback(
this.flush.bind(this),
{ timeout: 5000 }
);
}
}
destroy() {
if (this.idleCallbackId !== null) {
cancelIdleCallback(this.idleCallbackId);
}
}
}
const analytics = new AnalyticsQueue();
analytics.track('page_view', { path: window.location.pathname });
analytics.track('component_loaded', { name: 'ProductCard', duration: 42 });
10. Zusammenfassung
requestIdleCallback ist das richtige Werkzeug, wenn Aufgaben erledigt werden müssen, aber nicht sofort erledigt werden müssen. Die Deadline-API gibt dem Callback präzise Kontrolle über das verfügbare Zeitfenster, und das kooperative Chunk-Pattern ermöglicht auch die Verarbeitung großer Datenmengen ohne Main-Thread-Blockade. Der timeout-Parameter schützt vor dem „Never Run"-Problem bei dauerhaft ausgelasteten Seiten und macht die API auch für semi-kritische Hintergrundaufgaben nutzbar.
Der wichtigste Praxishinweis: requestIdleCallback ist nicht in Safari verfügbar – ein Polyfill oder Fallback ist für produktionsreife Implementierungen Pflicht. Die häufigsten Einsatzgebiete sind Analytics-Batching, Prefetch-Operationen, lokale Suche-Indizierung, Cache-Cleanup und Lazy Hydration von nicht-sichtbaren Komponenten. Für DOM-freie rechenintensive Aufgaben sind Web Workers die stärkere Alternative. Die Kombination von rIC für DOM-Hintergrundaufgaben und Web Workers für Compute-intensive Verarbeitung ist die skalierbare Architektur für Performance-kritische Webanwendungen.
Mironsoft
JavaScript Performance, Task-Scheduling und Browser-Architektur
Hintergrundaufgaben, die den Nutzer nicht stören?
Wir analysieren eure JavaScript-Architektur auf Main-Thread-Blockaden, identifizieren Kandidaten für requestIdleCallback und bauen robuste Task-Queues mit Fallbacks für alle Browser.
Thread-Analyse
Long Tasks identifizieren und in rIC-kompatible Chunks aufteilen
Queue-Architektur
Task-Prioritäten, Fallbacks und Lifecycle-Management sauber implementieren
Cross-Browser
Safari-Fallbacks und robuste Polyfills für alle Produktions-Umgebungen
requestIdleCallback — Das Wichtigste auf einen Blick
Idle-Scheduling
Browser führt Callback in Leerlaufphasen aus. Niedrigste Priorität – pausiert bei jeder Nutzerinteraktion oder Animation, um UI-Responsiveness zu wahren.
Deadline-API
timeRemaining() in Schleife prüfen, bei 0 unterbrechen und neu planen. didTimeout auswerten für Pflicht-Ausführung nach timeout-Ablauf.
Safari-Fallback
rIC ist in Safari nicht implementiert. Immer Polyfill oder setTimeout-Fallback einbauen. Wrapper-Funktion für plattformübergreifenden Code.
Ideal für
Analytics-Batching, Prefetch, lokale Suche, Cache-Cleanup, Lazy Hydration. Nicht für DOM-freie Schwerlast – dort Web Workers bevorzugen.