JS
() =>
JavaScript · Browser-APIs · Task-Scheduling · Performance
requestIdleCallback
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.

10 Min. Lesezeit rIC · Deadline-API · Idle-Budget · Polyfill Chrome, Firefox (kein Safari ohne Polyfill)

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.

11. FAQ: requestIdleCallback

1Was ist requestIdleCallback?
API für Callbacks in Leerlaufphasen des Main Threads. Ideal für nicht-kritische Hintergrundaufgaben wie Analytics, Prefetch und Cache-Cleanup.
2Safari-Unterstützung?
Keine. Immer Polyfill oder setTimeout-Fallback einbauen. Wrapper-Funktion mit window.requestIdleCallback-Check reicht für die meisten Fälle.
3Was macht timeRemaining()?
Gibt verbleibende Millisekunden im Idle-Fenster zurück. In Schleife prüfen – bei 0 unterbrechen und für nächstes Idle-Fenster neu planen.
4Was bedeutet didTimeout?
true wenn Callback wegen timeout-Ablauf erzwungen – nicht wegen echter Idle-Zeit. Minimale Arbeit erledigen und schnell zurückkehren.
5rIC vs. rAF – wann was?
rIC für Hintergrundaufgaben ohne visuelle Dringlichkeit. rAF für Aufgaben, die vor dem nächsten Render abgeschlossen sein müssen.
6Das 50-ms-Limit?
Browser begrenzt Idle-Fenster auf max. 50 ms. Aufgaben länger als 50 ms sind wahrnehmbar als UI-Verzögerung. Kooperatives Unterbrechen bei timeRemaining() == 0 ist Pflicht.
7Memory-Leaks vermeiden?
Handle speichern, cancelIdleCallback im Unmount/disconnectedCallback aufrufen. Verhindert Manipulation entfernter DOM-Elemente.
8Kann rIC niemals ausgeführt werden?
Ohne timeout: ja. timeout-Parameter garantiert Ausführung innerhalb des gesetzten Zeitraums, auch bei dauerhaft ausgelasteter Seite.
9Web Worker besser als rIC?
Für DOM-freie Schwerlast: ja. Web Workers laufen off-thread und blockieren den Main Thread nicht – unabhängig von der Laufzeit.
10Lange Aufgaben in Chunks aufteilen?
Array von Micro-Tasks oder Generator. Pro Callback so viele Tasks wie timeRemaining() erlaubt. Jeder Task unter 5 ms. Rest mit neuem rIC planen.