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.
Inhaltsverzeichnis
- 1. Warum setTimeout(0) und requestIdleCallback keine echte Priorisierung sind
- 2. Die drei Prioritaetsstufen: user-blocking, user-visible, background
- 3. Grundlegende Nutzung: ein priorisierter Task in wenigen Zeilen
- 4. TaskController: Prioritaet nachtraeglich aendern und Tasks abbrechen
- 5. Vergleich mit requestIdleCallback: Determinismus statt reinem Leerlauf-Warten
- 6. Vergleich mit setTimeout(0): dieselbe Warteschlange wie Rendering statt blinder Reihenfolge
- 7. Praxisbeispiel: grosse Datensaetze in Chunks verarbeiten, ohne die Seite einzufrieren
- 8. Praxisbeispiel: Vorabladen mit Hintergrund-Prioritaet, ohne Eingaben zu verzoegern
- 9. Browser-Support und eine sinnvolle Fallback-Kette
- 10. Zusammenfassung
- 11. FAQ
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.