Ein Klick, der erst nach einer gefühlten Ewigkeit reagiert, ist einer der häufigsten Frustmomente im Web – und fast immer steckt dieselbe Ursache dahinter: Der Hauptthread des Browsers ist mit einer langen JavaScript-Aufgabe beschäftigt und kommt nicht dazu, auf die Eingabe zu reagieren. Genau das misst der Core Web Vital INP, die Interaction to Next Paint. Solange ein einzelner Task läuft, kann der Browser weder den Klick verarbeiten noch das nächste Bild zeichnen; die Seite wirkt eingefroren. Die Lösung klingt einfach und ist es im Kern auch: lange Aufgaben in kleine Häppchen zerlegen und den Hauptthread zwischen den Häppchen kurz freigeben, damit wartende Klicks sofort drankommen. Lange war dieses Freigeben umständlich und mit Nebenwirkungen behaftet. Mit scheduler.yield() steht seit Chrome 129 (Chrome for Developers) eine Funktion bereit, die genau dafür gemacht ist – und die 2026 in immer mehr Browsern verfügbar ist. Dieser Beitrag zeigt, wann eine Aufgabe zum Problem wird, wie scheduler.yield() den Hauptthread freigibt, warum es besser funktioniert als der alte setTimeout-Trick und wie Sie den Effekt in Ihren Felddaten nachweisen. In unserer Core-Web-Vitals-Optimierung ist das Zerlegen langer Tasks einer der wirksamsten Hebel für einen guten INP-Wert.
Das Wichtigste in Kürze
- Der Browser erledigt fast alles auf einem einzigen Hauptthread. Jede Aufgabe ab 50 Millisekunden (web.dev) gilt als Long Task; solange sie läuft, bleiben Eingaben liegen, und bei 60 Hertz bleiben pro Bild nur rund 16 Millisekunden (web.dev).
- Als gut gilt ein INP unter 200 Millisekunden (web.dev), ausgewertet am 75. Perzentil (web.dev). Seit März 2024 (web.dev) ist INP ein Core Web Vital und misst anders als der frühere First Input Delay die gesamte Zeit bis zum sichtbaren Ergebnis.
- scheduler.yield() gibt den Hauptthread frei und setzt die eigene Arbeit danach bevorzugt fort, weil die Fortsetzung vorn in die Warteschlange kommt. Verfügbar ist die Funktion in Chromium-basierten Browsern seit Chrome 129 (Chrome for Developers).
- Der alte setTimeout-Weg reiht die Fortsetzung ans Ende der Warteschlange und unterliegt nach 5 verschachtelten Timern einer Mindestverzögerung von 4 Millisekunden (MDN). Als Fallback taugt er, als Standardweg nicht mehr.
- Nicht nach jeder Iteration übergeben: Sinnvoll ist ein Zeitbudget von rund 50 Millisekunden (web.dev), ergänzt um navigator.scheduling.isInputPending(). Geprüft wird die Total Blocking Time im Labor und der INP in den Felddaten.
Warum lange Tasks die Reaktion blockieren
Der Browser erledigt fast alles auf einem einzigen Hauptthread: Er wertet JavaScript aus, berechnet das Layout, zeichnet Bilder und verarbeitet Eingaben wie Klicks, Tastendrücke und Scrollbewegungen. Weil dieser Thread nur eine Sache zur Zeit tun kann, gilt: Solange er mit einer langen Berechnung beschäftigt ist, bleibt jede Eingabe liegen. Das Chrome-Team zählt jede Aufgabe ab 50 Millisekunden (web.dev) als Long Task, weil ab dieser Dauer die Gefahr steigt, dass eine Interaktion nicht rechtzeitig beantwortet wird. Das RAIL-Modell empfiehlt, auf Eingaben innerhalb von 50 Millisekunden (web.dev) zu reagieren, damit sich die Reaktion unmittelbar anfühlt. Zum Vergleich: Bei einem Bildschirm mit 60 Hertz bleiben pro Bild nur rund 16 Millisekunden (web.dev) Zeit – ein Task von mehreren Hundert Millisekunden verschluckt also dutzende Frames am Stück und lässt die Seite ruckeln.
Der INP fasst diese Erfahrung in einer Zahl zusammen. Er beobachtet über den gesamten Seitenbesuch hinweg alle Klicks, Tastatureingaben und Tap-Gesten und meldet am Ende annähernd die längste beobachtete Verzögerung – ausgewertet am 75. Perzentil (web.dev) der Interaktionen, damit einzelne Ausreißer den Wert nicht verfälschen. Als gut gilt ein INP unter 200 Millisekunden (web.dev); zwischen 200 und 500 Millisekunden (web.dev) besteht Handlungsbedarf, und über 500 Millisekunden (web.dev) ist der Wert schlecht. Seit März 2024 (web.dev) ist INP einer der drei Core Web Vitals und hat den alten First Input Delay abgelöst, dessen Schwelle noch bei 100 Millisekunden (web.dev) lag. Der Unterschied ist entscheidend: FID maß nur die Verzögerung bis zum Beginn der Verarbeitung, INP dagegen die gesamte Zeit bis zum sichtbaren Ergebnis – und deckt lange Tasks damit gnadenlos auf.
Was genau ist ein Long Task?
Was scheduler.yield anders macht als setTimeout
Die Idee, den Hauptthread zwischenzeitlich freizugeben, ist nicht neu. Klassisch behalf man sich mit setTimeout und null Millisekunden Verzögerung: Man unterbrach die Arbeit, reihte den Rest über einen Timer neu ein und gab dem Browser so die Chance, dazwischen Eingaben zu verarbeiten. Der Trick funktioniert, hat aber zwei Haken. Erstens landet der fortgesetzte Teil am Ende der Aufgaben-Warteschlange – hinter allem, was der Browser sonst noch eingeplant hat, inklusive fremder Skripte. Zweitens erzwingt die HTML-Spezifikation nach 5 verschachtelten Timern eine Mindestverzögerung von 4 Millisekunden (MDN), die sich bei vielen Häppchen aufsummiert. Das Ergebnis: Die Arbeit gibt zwar nach, wird danach aber möglicherweise lange nicht fortgesetzt.
scheduler.yield() löst genau dieses Problem. Der Aufruf gibt den Hauptthread ebenfalls frei, sodass der Browser wartende Klicks und ausstehende Renderarbeit erledigen kann – doch anschließend setzt er die eigene Aufgabe bevorzugt fort. Die Fortsetzung wird nicht ans Ende, sondern an den Anfang der Warteschlange gestellt, sodass die begonnene Arbeit nicht von neu eingetroffenen Aufgaben überholt wird. Man bekommt so das Beste aus beiden Welten: Der Browser bleibt reaktionsfreudig, und die Berechnung läuft trotzdem zügig zu Ende. scheduler.yield() gibt ein Promise zurück und wird mit await verwendet – der Code liest sich dadurch fast wie eine gewöhnliche Schleife. Verfügbar ist die Funktion in Chromium-basierten Browsern seit Chrome 129 (Chrome for Developers), das im September 2024 (Chrome for Developers) erschien, und sie wird 2026 zunehmend breiter unterstützt.
// Übergabe an den Hauptthread, mit Fallback für ältere Browser
async function yieldToMain() {
if ('scheduler' in window && 'yield' in scheduler) {
return scheduler.yield();
}
// Fallback: Fortsetzung ans Ende der Aufgaben-Warteschlange
return new Promise((resolve) => setTimeout(resolve, 0));
}Damit die Technik auch in Browsern greift, die scheduler.yield() noch nicht kennen, kapselt man den Aufruf in eine kleine Hilfsfunktion mit Feature-Erkennung. Ist die Funktion vorhanden, wird sie genutzt; andernfalls fällt der Code sauber auf den bewährten setTimeout-Weg zurück. So profitieren moderne Browser vom bevorzugten Fortsetzen, während ältere zumindest die grundlegende Freigabe erhalten – ohne dass die Anwendung an einer fehlenden Funktion scheitert.
Eine lange Schleife in Häppchen zerlegen
Der häufigste Auslöser für lange Tasks ist eine Schleife, die über viele Elemente iteriert und für jedes eine nicht ganz billige Operation ausführt – etwa das Aufbereiten einer großen Produktliste, das Verarbeiten einer umfangreichen JSON-Antwort oder das Erzeugen vieler DOM-Knoten. Führt der Code die gesamte Schleife am Stück aus, entsteht ein einziger langer Task. Die Lösung besteht darin, nach jedem Arbeitsschritt – oder nach einer Gruppe von Schritten – kurz an den Hauptthread zu übergeben. Im einfachsten Fall genügt ein await auf die Hilfsfunktion mitten in der Schleife.
async function processEntries(entries) {
for (const entry of entries) {
processEntry(entry); // teure Einzelarbeit
await yieldToMain(); // Hauptthread freigeben
}
}Nicht nach jeder Iteration yielden
Nur yielden, wenn Input wartet
Noch feiner lässt sich steuern, indem man nur dann übergibt, wenn tatsächlich eine Eingabe wartet. Die Browser-Funktion navigator.scheduling.isInputPending() verrät, ob ein Klick oder Tastendruck in der Warteschlange liegt. Kombiniert man sie mit einem Zeitbudget, entsteht ein Muster, das den Hauptthread nur dann freigibt, wenn es sich lohnt: entweder weil das Budget erschöpft ist oder weil ein Nutzer gerade interagiert. In ruhigen Phasen läuft die Arbeit ohne Unterbrechung durch, in hektischen wird sofort reagiert. Der Fragezeichen-Operator hält den Code dabei robust, falls ein Browser die Funktion nicht anbietet.
async function inChunks(entries) {
let lastYield = performance.now();
for (const entry of entries) {
processEntry(entry);
// Nur yielden, wenn 50 ms verbraucht sind oder Eingabe wartet
if (performance.now() - lastYield > 50 ||
navigator.scheduling?.isInputPending?.()) {
await yieldToMain();
lastYield = performance.now();
}
}
}Große Datenmengen aufbereiten
Filtern, Sortieren oder Umrechnen langer Listen im Browser ist ein Klassiker unter den langen Tasks. Ein Zeitbudget mit Übergabe hält die Oberfläche bedienbar, während im Hintergrund gerechnet wird.
Viel DOM auf einmal bauen
Wird eine umfangreiche Tabelle oder Liste in einem Rutsch erzeugt, blockiert das Einfügen den Thread. In Häppchen aufgeteilt erscheint der Inhalt gestaffelt, aber die Seite bleibt reaktionsfähig.
Beim Laden viel initialisieren
Init-Skripte, Hydration und nachgeladene Bausteine erledigen oft viel auf einmal. Die Arbeit über mehrere Chunks zu verteilen, verhindert einen langen Block direkt nach dem ersten Rendern.
Alternativen und der Fallback
scheduler.yield() ist nicht das einzige Werkzeug zur Aufgabenplanung. Die verwandte Funktion scheduler.postTask() plant ganze Aufgaben mit einer von 3 Prioritätsstufen (MDN) ein – user-blocking, user-visible und background – und eignet sich, um Arbeit von vornherein nach Wichtigkeit zu staffeln. requestIdleCallback wiederum schiebt unkritische Arbeit in die Leerlaufzeiten des Browsers. Für das reine Aufteilen einer langen, zusammenhängenden Aufgabe ist scheduler.yield() aber die direkteste Wahl, weil es die begonnene Arbeit bevorzugt fortsetzt. In Browsern ohne Unterstützung bleibt der setTimeout-Fallback – er gibt zwar nur ans Ende der Warteschlange zurück, verhindert aber immerhin den langen Block.
| Verfahren | Gibt Hauptthread frei | Fortsetzung | Bester Einsatz |
|---|---|---|---|
| setTimeout(fn, 0) | enthalten | Ende der Queue, Clamp ab 4 ms | Fallback für ältere Browser |
| scheduler.postTask() | Plant ganze Aufgaben | Nach Prioritätsstufe | Arbeit nach Wichtigkeit staffeln |
| requestIdleCallback() | In Leerlaufzeiten | Wenn Zeit übrig ist | Unkritische Hintergrundarbeit |
| scheduler.yield() | enthalten | Bevorzugt, vorne in der Queue | Lange Aufgabe in Häppchen teilen |
Der Kern in einem Satz
So messen Sie den Effekt
Ob das Aufteilen wirkt, lässt sich mit frei verfügbaren Werkzeugen prüfen. Im Labor zeigt der Lighthouse-Bericht die Total Blocking Time und markiert lange Tasks; sinkt die Zahl der Blöcke über 50 Millisekunden (web.dev), war die Aufteilung erfolgreich. Das Performance-Panel der Chrome-Entwicklerwerkzeuge stellt jeden Task als Balken dar – lange Aufgaben werden markiert und lassen sich vor und nach der Änderung direkt vergleichen. Für die Ursachensuche in echten Sitzungen liefert die Long Animation Frames API eine genaue Zuordnung, welches Skript einen Frame aufgehalten hat; wie sich damit INP-Bremsen im Frame aufspüren lassen, behandeln wir gesondert. Die quelloffene web-vitals-Bibliothek meldet den INP zusätzlich aus den Felddaten echter Nutzer.
- Lange Tasks im Lighthouse-Bericht oder im Performance-Panel identifizieren
- Die auslösende Funktion bestimmen – meist eine Schleife oder ein Init-Block
- Eine yieldToMain-Hilfsfunktion mit Feature-Erkennung und Fallback anlegen
- Die Arbeit mit einem Zeitbudget von rund 50 Millisekunden in Häppchen teilen
- Nur bei erschöpftem Budget oder wartender Eingabe an den Hauptthread übergeben
- Vorher und nachher messen – Total Blocking Time im Labor, INP in den Felddaten
Wichtig ist, im Labor mit realistischer Drosselung zu messen: Ein schneller Bürorechner verbirgt genau die langen Tasks, die auf einem durchschnittlichen Smartphone zum Problem werden. Deshalb gehört zu jeder Optimierung der Blick ins Feld – nur dort zeigt sich, was echte Nutzer auf ihren Geräten erleben. Und weil sich neue lange Tasks leicht wieder einschleichen, etwa durch ein zusätzliches Skript oder eine gewachsene Datenmenge, lohnt es sich, die Werte dauerhaft zu überwachen und Regressionen früh zu stoppen, wie wir es beim Performance-Budget im CI beschreiben.
Vom Task-Splitting zur stabilen INP
Das Aufteilen langer Tasks ist selten die einzige Maßnahme, aber oft die wirkungsvollste für die Reaktionsschnelligkeit. Häufig stammen die längsten Aufgaben nicht einmal aus dem eigenen Code, sondern aus eingebundenen Fremdskripten – Werbe-, Analyse- oder Chat-Bausteinen, die beim Laden viel auf einmal erledigen. Wie sich solche Drittanbieter-Skripte entschlacken lassen, ist deshalb der natürliche zweite Schritt. Ergänzend hilft ein bewusstes JavaScript-Performance-Budget, damit gar nicht erst zu viel Code auf einmal läuft. Und wer den INP im Zusammenhang mit den beiden anderen Kennzahlen verstehen will, findet die Einordnung im Beitrag zu den Core Web Vitals 2026 sowie im Überblick zum INP gezielt optimieren.
Reaktionsschnelligkeit entsteht nicht dadurch, dass wir alles schneller rechnen, sondern dass wir den Browser im richtigen Moment atmen lassen. Ein langer Task, klug in Häppchen zerlegt, fühlt sich für den Nutzer sofort flüssiger an – und der INP folgt dieser Erfahrung.
Genau so gehen wir in einem strukturierten Performance-Audit vor: lange Tasks aufspüren, die auslösenden Funktionen benennen und sie mit scheduler.yield() und einem sauberen Fallback in reaktionsfreundliche Häppchen zerlegen – anschließend prüfen wir in Ihren Felddaten, ob der INP dauerhaft im grünen Bereich bleibt. Welche Leistungen dazugehören, zeigt unsere Übersicht der Performance-Leistungen; die konkreten Frontend-Hebel bündeln wir in der Frontend-Optimierung.
Verwandte Artikel
JavaScript und Performance Budgets: Weniger Code
JavaScript-Einfluss auf INP und Ladezeit minimieren: Performance Budgets, Code Splitting, Tree Shaking und Third-Party-Management für Core Web Vitals.
Speicherlecks im Frontend: wenn die Seite träge wird
Die Oberfläche startet schnell und wird nach zwanzig Minuten zäh: wie Speicherlecks im Frontend entstehen, wie man sie im Verlauf misst und wieder loswird.
Web Worker: Main-Thread entlasten und INP verbessern
Rechenintensives JavaScript in einen Web Worker auslagern hält den Main-Thread frei, senkt die Total Blocking Time und bringt INP unter die 200-ms-Schwelle.