scheduler.postTask(): Prioritaeten fuer eigene JavaScript-Tasks steuern
AI generated
JS
() =>
JavaScript · Scheduling · Performance
scheduler.postTask()
Eigene Tasks gezielt priorisieren, statt sie blind in die Event-Loop zu werfen

setTimeout(0) und requestIdleCallback geben dem Browser keine echte Information darueber, wie wichtig ein Task tatsaechlich ist. scheduler.postTask() schliesst diese Luecke mit expliziten Prioritaetsstufen: user-blocking, user-visible und background, sodass eigener Code sich in dieselbe Warteschlange einreiht, die auch Rendering und Eingaben verwaltet, statt gegen sie zu konkurrieren.

14 Min. Lesezeit user-blocking · user-visible · background TaskController · Priority Scheduling

1. Warum setTimeout(0) und requestIdleCallback keine echte Priorisierung sind

setTimeout(0) verschiebt einen Task lediglich ans Ende der aktuellen Task-Warteschlange, gibt dem Browser dabei aber keinerlei Information darueber, wie dringend die Ausfuehrung ist. Ein mit setTimeout(0) geplanter Task konkurriert gleichberechtigt mit jedem anderen Task in der Queue, unabhaengig davon, ob er fuer die Nutzerinteraktion kritisch ist oder problemlos zehn Sekunden warten koennte. Zusaetzlich klemmt der Browser verschachtelte setTimeout-Aufrufe nach wenigen Ebenen auf eine Mindestverzoegerung von vier Millisekunden.

requestIdleCallback wiederum laeuft ausschliesslich dann, wenn der Browser tatsaechlich Leerlaufzeit hat, mit einem deadline-Objekt, das die verbleibende Zeit angibt. Das Problem: Auf vielbeschaeftigten Seiten mit viel Rendering und Nutzerinteraktion gibt es kaum Leerlaufzeit, wodurch geplante Callbacks unvorhersehbar lange aufgeschoben werden. Zudem bietet die API keinerlei Abstufung zwischen mehreren gleichzeitig wartenden Tasks, alle konkurrieren um dieselbe knappe Leerlaufzeit.

2. Die drei Prioritaetsstufen: user-blocking, user-visible, background

scheduler.postTask() kennt drei feste Prioritaetsstufen. user-blocking ist fuer Arbeit reserviert, die der Nutzer als direkte Reaktion auf seine Eingabe erwartet, etwa das Aktualisieren eines Eingabefelds waehrend des Tippens oder eine Animation, die unmittelbar auf einen Klick folgt. Diese Prioritaet sollte sparsam verwendet werden, weil sie tatsaechlich vor Rendering-Arbeit eingeplant wird.

user-visible ist die Standardpriotitaet und passt fuer Arbeit, die zwar sichtbar, aber nicht zeitkritisch ist, etwa das Nachladen von Inhalten unterhalb der Sichtflaeche. background schliesslich eignet sich fuer Arbeit, die der Nutzer gar nicht direkt bemerkt, etwa Analytics-Uebertragungen, Log-Aggregation oder das Vorbereiten von Daten, die erst in einigen Sekunden gebraucht werden.

3. Grundlegende Nutzung: ein priorisierter Task in wenigen Zeilen

scheduler.postTask(callback, options) nimmt eine Callback-Funktion sowie ein Optionsobjekt mit priority, signal und optional delay entgegen und gibt ein Promise zurueck, das mit dem Rueckgabewert des Callbacks aufgeloest wird. Das macht die API sofort kompatibel mit async/await, ganz ohne zusaetzliches Promise-Wrapping wie bei setTimeout noch noetig.

Der Browser reiht den Task intern in dieselbe zentrale Scheduler-Warteschlange ein, die auch fuer Rendering, Style-Berechnung und Eingabeverarbeitung zustaendig ist. Dadurch kann der Browser tatsaechlich fundierte Entscheidungen treffen, welcher Task als naechstes drankommt, statt Tasks strikt nach Ankunftsreihenfolge abzuarbeiten wie bei einer klassischen setTimeout-Warteschlange.


// Ein Task mit niedriger Prioritaet fuer unkritische Analytics
await scheduler.postTask(() => {
  sendAnalyticsBatch(pendingEvents);
}, { priority: 'background' });

// Ein Task mit hoher Prioritaet direkt nach einer Nutzereingabe
scheduler.postTask(() => {
  updateSearchSuggestions(query);
}, { priority: 'user-blocking' });

4. TaskController: Prioritaet nachtraeglich aendern und Tasks abbrechen

Ein TaskController erzeugt ein TaskSignal, das anstelle eines gewoehnlichen AbortSignal an die signal-Option von postTask uebergeben wird. Weil TaskSignal von AbortSignal erbt, funktioniert controller.abort() genauso wie bei fetch(), ein laufender oder noch nicht gestarteter Task laesst sich damit jederzeit sauber abbrechen.

Der entscheidende Zusatznutzen ist controller.setPriority(neuePrioritaet): Damit laesst sich die Prioritaet eines bereits eingeplanten Tasks nachtraeglich aendern, etwa wenn ein zunaechst als background eingestufter Vorabladevorgang ploetzlich user-visible relevant wird, weil der Nutzer zu dem betreffenden Bereich der Seite scrollt. Ohne TaskController muesste der urspruengliche Task abgebrochen und komplett neu eingeplant werden.


const controller = new TaskController({ priority: 'background' });

scheduler.postTask(() => ladeVorschauBilder(), {
  signal: controller.signal,
});

// Nutzer scrollt in den relevanten Bereich: Prioritaet anheben
sichtbarkeitsBeobachter.addEventListener('intersect', () => {
  controller.setPriority('user-visible');
});

5. Vergleich mit requestIdleCallback: Determinismus statt reinem Leerlauf-Warten

requestIdleCallback garantiert lediglich, dass der Callback irgendwann laeuft, wenn der Browser Leerlaufzeit findet, mit einer optionalen timeout-Option als letzter Notausgang. Auf ereignisreichen Seiten mit staendiger Nutzerinteraktion kann diese Leerlaufzeit faktisch ausbleiben, wodurch Callbacks weit spaeter als gewuenscht oder erst nach Erreichen des Timeouts laufen, was den Zweck der Priorisierung untergraebt.

scheduler.postTask() dagegen wird vom selben zentralen Scheduler verwaltet, der auch Eingabeverarbeitung und Rendering priorisiert, wodurch die Browser-Engine fundiertere Entscheidungen treffen kann, welcher konkurrierende Task Vorrang bekommt. Ausserdem unterstuetzt requestIdleCallback historisch nicht alle Browser gleich zuverlaessig, waehrend postTask von Anfang an als bewusst spezifizierte, plattformweite Scheduling-Primitve entworfen wurde.

6. Vergleich mit setTimeout(0): dieselbe Warteschlange wie Rendering statt blinder Reihenfolge

setTimeout(0) reiht einen Task strikt nach Ankunftszeit in die Makrotask-Warteschlange ein, ganz ohne jede Ruecksicht darauf, ob gerade ein zeitkritischer Rendering-Frame ansteht. Bei verschachtelten Aufrufen erzwingt die minimale Verzoegerung von vier Millisekunden zudem einen kuenstlichen Flaschenhals, der mit echter Prioritaet nichts zu tun hat, sondern rein historisch bedingt ist.

scheduler.postTask() hingegen erlaubt es dem Browser, einen user-blocking-Task tatsaechlich vor einem laufenden background-Task einzuschieben, selbst wenn der background-Task zuerst eingeplant wurde. Diese Umsortierung nach Prioritaet statt reiner Ankunftsreihenfolge ist der zentrale Unterschied, der postTask fuer gemischte Workloads deutlich praediktierbarer macht.


// setTimeout: strikt nach Ankunftsreihenfolge, keine Prioritaet
setTimeout(() => grosseBerechnung(), 0);
setTimeout(() => klickReaktion(), 0); // wartet trotz Dringlichkeit

// postTask: der Browser darf umsortieren
scheduler.postTask(() => grosseBerechnung(), { priority: 'background' });
scheduler.postTask(() => klickReaktion(), { priority: 'user-blocking' });

7. Praxisbeispiel: grosse Datensaetze in Chunks verarbeiten, ohne die Seite einzufrieren

Beim Verarbeiten eines grossen Arrays, etwa beim clientseitigen Filtern und Sortieren mehrerer tausend Zeilen, blockiert eine einzelne synchrone Schleife den Main Thread so lange, dass Eingaben und Scrollen spuerbar ruckeln. Die Loesung ist, die Verarbeitung in kleine Haeppchen zu zerlegen und zwischen den Haeppchen dem Scheduler bewusst die Kontrolle zurueckzugeben.

Jeder Chunk wird als eigener scheduler.postTask()-Aufruf mit user-visible-Prioritaet eingeplant. Weil jeder Aufruf ein eigener Task ist, kann der Browser zwischen den Chunks jederzeit einen dringenderen user-blocking-Task einschieben, etwa die Reaktion auf einen Klick, ohne auf das Ende der gesamten Verarbeitung warten zu muessen.


async function verarbeiteInChunks(eintraege, chunkGroesse = 200) {
  for (let i = 0; i < eintraege.length; i += chunkGroesse) {
    const chunk = eintraege.slice(i, i + chunkGroesse);
    await scheduler.postTask(() => verarbeiteChunk(chunk), {
      priority: 'user-visible',
    });
  }
}

8. Praxisbeispiel: Vorabladen mit Hintergrund-Prioritaet, ohne Eingaben zu verzoegern

Beim Vorabladen von Bildern, Skripten oder Daten fuer eine wahrscheinlich naechste Nutzeraktion, etwa das Vorbereiten der naechsten Produktseite in einem Karussell, soll die Arbeit passieren, darf aber niemals eine gleichzeitig eintreffende Nutzereingabe verzoegern. Genau dafuer ist die Prioritaet background gedacht.

Ein mit background eingeplanter Task wird vom Scheduler konsequent hinter jeden user-blocking- und user-visible-Task einsortiert, laeuft aber trotzdem zuverlaessiger und fruehzeitiger als ein Aequivalent per requestIdleCallback, weil der zentrale Scheduler die verbleibende Kapazitaet aktiv einplant, statt rein passiv auf Leerlauf zu warten.


function planeVorabladen(naechsteProduktId) {
  scheduler.postTask(async () => {
    const bild = new Image();
    bild.src = `/produkte/${naechsteProduktId}/hauptbild.jpg`;
    await bild.decode();
  }, { priority: 'background' });
}

9. Browser-Support und eine sinnvolle Fallback-Kette

scheduler.postTask() ist inzwischen in allen Chromium-basierten Browsern verfuegbar, in Firefox und Safari fehlte lange Zeit die Unterstuetzung, weshalb produktiver Code eine Feature-Detection mit typeof scheduler !== 'undefined' voranstellen sollte. Als Fallback bietet sich eine Kette an: zuerst versuchen, postTask zu nutzen, sonst auf setTimeout mit einer an die Prioritaet gekoppelten Verzoegerung ausweichen.

Eine solche Fallback-Funktion bildet die drei Prioritaetsstufen naeherungsweise auf unterschiedliche setTimeout-Verzoegerungen ab, etwa null Millisekunden fuer user-blocking und eine spuerbar hoehere Verzoegerung fuer background. Das ist zwar kein vollwertiger Ersatz fuer echtes Scheduler-basiertes Priorisieren, verhindert aber, dass aeltere Browser komplett ohne Priorisierungslogik dastehen.

Prioritaet Typischer Anwendungsfall Verhaeltnis zu Rendering Vergleichbar mit
user-blocking Direkte Reaktion auf Tastatur- oder Klick-Eingabe Vor Rendering-Arbeit eingeplant Synchroner Event-Handler
user-visible (Standard) Sichtbare, aber nicht sofort kritische Aktualisierung Zwischen Rendering-Frames eingeplant requestAnimationFrame fuer Logik
background Analytics, Vorabladen, Log-Aggregation Nach allen sichtbaren Tasks eingeplant requestIdleCallback, aber deterministischer
Kein Scheduler verfuegbar (Fallback) Aeltere Browser ohne scheduler.postTask Reine Ankunftsreihenfolge in setTimeout setTimeout(0) mit gestaffelter Verzoegerung

Mironsoft

Moderne Browser-APIs, Performance und wartbares JavaScript

JavaScript, das im echten Browser robust bleibt, nicht nur im Tutorial?

Wir prüfen bestehenden Frontend-Code auf veraltete Patterns, unnötige Bibliotheken und Performance-Fallen und ersetzen sie durch moderne, native Browser-APIs, die weniger Bundle-Gewicht und weniger Wartungslast bedeuten.

Code-Review

Veraltete Patterns, unnötige Dependencies und Memory Leaks systematisch aufspüren.

Performance-Optimierung

Bundle-Größe, Ladezeit und Runtime-Performance mit modernen APIs verbessern.

Modernisierung

Native Browser-APIs statt schwerer Bibliotheken gezielt einführen.

10. Zusammenfassung

scheduler.postTask(): Das Wichtigste auf einen Blick

Kernidee

scheduler.postTask() vergibt explizite Prioritaeten statt Tasks blind nach Ankunftsreihenfolge abzuarbeiten.

Drei Stufen

user-blocking, user-visible und background bilden reale Dringlichkeit auf Scheduler-Entscheidungen ab.

TaskController

Erlaubt nachtraegliches Umpriorisieren und sauberes Abbrechen laufender oder wartender Tasks.

Vorteil gegenueber Alternativen

Nutzt denselben Scheduler wie Rendering und Eingaben, dadurch deterministischer als requestIdleCallback.

11. FAQ: scheduler.postTask(): Das Wichtigste auf einen Blick

1Was ist der Hauptunterschied zwischen scheduler.postTask() und setTimeout(0)?
setTimeout(0) reiht Tasks strikt nach Ankunftszeit ein und kennt keine Prioritaet, waehrend postTask dem Browser explizit mitteilt, wie dringend ein Task ist, sodass dringendere Tasks vor bereits eingereihten, weniger dringenden Tasks laufen koennen.
2Wann sollte ich user-blocking als Prioritaet waehlen?
Nur fuer Arbeit, die der Nutzer als unmittelbare Reaktion auf seine eigene Eingabe erwartet, etwa das Aktualisieren eines Suchfelds waehrend des Tippens. Diese Prioritaet wird tatsaechlich vor Rendering-Arbeit eingeplant und sollte deshalb sparsam eingesetzt werden.
3Ist user-visible die richtige Standardwahl, wenn ich unsicher bin?
In den meisten Faellen ja, weil diese Prioritaet ohnehin der Default ist, wenn keine priority-Option angegeben wird, und einen guten Kompromiss zwischen Dringlichkeit und Rendering-Freundlichkeit darstellt.
4Wie unterscheidet sich background von requestIdleCallback?
Beide zielen auf unkritische Hintergrundarbeit ab, background lauft aber ueber denselben zentralen Scheduler wie Rendering und Eingaben und wird dadurch deterministischer eingeplant, waehrend requestIdleCallback rein passiv auf tatsaechliche Leerlaufzeit wartet.
5Wozu genau dient ein TaskController?
Ein TaskController erzeugt ein TaskSignal, mit dem sich ein Task sowohl abbrechen als auch nachtraeglich per setPriority auf eine andere Prioritaetsstufe umsetzen laesst, ohne den Task neu einplanen zu muessen.
6Kann ich await mit scheduler.postTask() verwenden?
Ja, postTask gibt ein Promise zurueck, das mit dem Rueckgabewert des Callbacks aufgeloest wird, wodurch sich await direkt verwenden laesst, ganz ohne zusaetzliches manuelles Promise-Wrapping.
7Unterstuetzen alle Browser scheduler.postTask()?
Chromium-basierte Browser unterstuetzen die API bereits laenger, Firefox und Safari haben lange gefehlt. Produktiver Code sollte deshalb eine Feature-Detection und eine setTimeout-basierte Fallback-Kette vorsehen.
8Wie teile ich grosse Datenverarbeitung sinnvoll in Chunks auf?
Jeder Chunk wird als eigener scheduler.postTask()-Aufruf eingeplant, meist mit user-visible-Prioritaet, sodass der Browser zwischen den Chunks dringendere Tasks wie Klick-Reaktionen einschieben kann, statt den Main Thread am Stueck zu blockieren.
9Ersetzt postTask Web Worker fuer rechenintensive Aufgaben?
Nein, postTask organisiert lediglich die Reihenfolge und Prioritaet von Tasks auf dem Main Thread, verlagert aber keine Arbeit auf einen anderen Thread. Fuer wirklich rechenintensive Arbeit bleibt ein Web Worker die richtige Wahl.
10Kann sich die Prioritaet eines Tasks waehrend der Wartezeit aendern?
Ja, ueber controller.setPriority() auf dem zugehoerigen TaskController laesst sich die Prioritaet eines noch nicht gestarteten Tasks jederzeit anpassen, etwa wenn sich die Relevanz einer Hintergrundaufgabe durch eine Nutzeraktion ploetzlich erhoeht.