Veröffentlicht: 6. März 2025
Eine Seite wirkt träge und reagiert nicht, wenn lange Aufgaben den Hauptthread belegen und ihn daran hindern, andere wichtige Aufgaben auszuführen, z. B. auf Nutzereingaben zu reagieren. So können selbst integrierte Formularsteuerelemente für Nutzer beschädigt erscheinen, als wäre die Seite eingefroren, ganz zu schweigen von komplexeren benutzerdefinierten Komponenten.
scheduler.yield()
ist eine Möglichkeit, dem Hauptthread zu weichen, damit der Browser alle ausstehenden Aufgaben mit hoher Priorität ausführen kann, und die Ausführung dann dort fortzusetzen, wo sie unterbrochen wurde. Dadurch bleibt eine Seite reaktionsschneller und der Messwert „Interaction to Next Paint“ (INP) wird verbessert.
scheduler.yield
bietet eine ergonomische API, die genau das tut, was sie verspricht: Die Ausführung der Funktion, in der sie aufgerufen wird, wird beim await scheduler.yield()
-Ausdruck pausiert und an den Hauptthread übergeben, wodurch die Aufgabe aufgeteilt wird. Die Ausführung des Rests der Funktion, die Fortsetzung der Funktion, wird in einem neuen Ereignisschleifen-Task geplant.
async function respondToUserClick() {
giveImmediateFeedback();
await scheduler.yield(); // Yield to the main thread.
slowerComputation();
}
Der besondere Vorteil von scheduler.yield
besteht darin, dass die Fortsetzung nach dem Yield vor der Ausführung anderer ähnlicher Aufgaben geplant ist, die von der Seite in die Warteschlange gestellt wurden. Die Fortsetzung einer Aufgabe hat Vorrang vor dem Starten neuer Aufgaben.
Funktionen wie setTimeout
oder scheduler.postTask
können auch verwendet werden, um Aufgaben aufzuteilen. Diese Fortsetzungen werden jedoch in der Regel nach allen bereits in der Warteschlange befindlichen neuen Aufgaben ausgeführt, was zu langen Verzögerungen zwischen dem Übergeben an den Hauptthread und dem Abschluss der Arbeit führen kann.
Priorisierte Fortsetzungen nach dem Übergeben
scheduler.yield
ist Teil der Prioritized Task Scheduling API. Als Webentwickler sprechen wir in der Regel nicht über die Reihenfolge, in der der Ereignis-Loop Aufgaben ausführt, in Bezug auf explizite Prioritäten. Die relativen Prioritäten sind jedoch immer vorhanden, z. B. ein requestIdleCallback
-Callback, der nach allen setTimeout
-Callbacks in der Warteschlange ausgeführt wird, oder ein ausgelöster Eingabeereignis-Listener, der normalerweise vor einer Aufgabe ausgeführt wird, die mit setTimeout(callback, 0)
in die Warteschlange gestellt wurde.
Die priorisierte Aufgabenplanung macht dies nur noch deutlicher. So lässt sich leichter feststellen, welche Aufgabe vor einer anderen ausgeführt wird, und die Prioritäten können bei Bedarf angepasst werden, um die Ausführungsreihenfolge zu ändern.
Wie bereits erwähnt, hat die fortgesetzte Ausführung einer Funktion nach dem Yielding mit scheduler.yield()
eine höhere Priorität als das Starten anderer Aufgaben. Das Leitkonzept besteht darin, dass die Fortsetzung einer Aufgabe zuerst ausgeführt werden sollte, bevor mit anderen Aufgaben fortgefahren wird. Wenn die Aufgabe korrekten Code enthält, der regelmäßig pausiert, damit der Browser andere wichtige Dinge tun kann (z. B. auf Nutzereingaben reagieren), sollte sie nicht dafür bestraft werden, dass sie pausiert, indem sie nach anderen ähnlichen Aufgaben priorisiert wird.
Hier ein Beispiel: Zwei Funktionen, die mit setTimeout
in verschiedenen Aufgaben ausgeführt werden sollen.
setTimeout(myJob);
setTimeout(someoneElsesJob);
In diesem Fall stehen die beiden setTimeout
-Aufrufe direkt nebeneinander. Auf einer echten Seite könnten sie jedoch an völlig unterschiedlichen Stellen aufgerufen werden, z. B. in einem Script von einem Drittanbieter und einem Script von einem Drittanbieter, die unabhängig voneinander Aufgaben einrichten, die ausgeführt werden sollen. Es könnten auch zwei Aufgaben aus separaten Komponenten sein, die tief im Scheduler Ihres Frameworks ausgelöst werden.
So könnte das in den DevTools aussehen:
myJob
wird als langwierige Aufgabe gekennzeichnet, wodurch der Browser während der Ausführung keine anderen Aktionen ausführen kann. Angenommen, es stammt aus einem selbst erstellten Script, können wir es so aufschlüsseln:
function myJob() {
// Run part 1.
myJobPart1();
// Yield with setTimeout() to break up long task, then run part2.
setTimeout(myJobPart2, 0);
}
Da myJobPart2
innerhalb von myJob
mit setTimeout
ausgeführt werden soll, die Planung aber nach der Planung von someoneElsesJob
erfolgt, sieht die Ausführung so aus:
Wir haben die Aufgabe mit setTimeout
aufgeteilt, damit der Browser in der Mitte von myJob
reaktionsschnell sein kann. Jetzt wird der zweite Teil von myJob
jedoch erst ausgeführt, nachdem someoneElsesJob
abgeschlossen ist.
In einigen Fällen ist das in Ordnung, aber in der Regel ist das nicht optimal. myJob
gab dem Hauptthread die Kontrolle zurück, damit die Seite weiterhin auf Nutzereingaben reagieren konnte, nicht, um den Hauptthread vollständig aufzugeben. Wenn someoneElsesJob
besonders langsam ist oder neben someoneElsesJob
noch viele andere Jobs geplant wurden, kann es lange dauern, bis die zweite Hälfte von myJob
ausgeführt wird. Das war wahrscheinlich nicht die Absicht des Entwicklers, als er setTimeout
zu myJob
hinzufügte.
Geben Sie scheduler.yield()
ein. Dadurch wird die Fortsetzung jeder Funktion, die sie aufruft, in eine Warteschlange mit etwas höherer Priorität als der Start anderer ähnlicher Aufgaben verschoben. Wenn myJob
so geändert wird, dass es verwendet wird:
async function myJob() {
// Run part 1.
myJobPart1();
// Yield with scheduler.yield() to break up long task, then run part2.
await scheduler.yield();
myJobPart2();
}
Die Ausführung sieht jetzt so aus:
Der Browser kann weiterhin reaktionsschnell sein, aber jetzt wird die Fortsetzung der Aufgabe myJob
vor dem Start der neuen Aufgabe someoneElsesJob
priorisiert. Daher ist myJob
abgeschlossen, bevor someoneElsesJob
beginnt. Dies entspricht viel eher der Erwartung, dass der Hauptthread die Kontrolle übernimmt, um die Reaktionsfähigkeit aufrechtzuerhalten, und nicht vollständig aufgegeben wird.
Prioritätsübernahme
Als Teil der größeren API für die priorisierte Aufgabenplanung lässt sich scheduler.yield()
gut mit den expliziten Prioritäten kombinieren, die in scheduler.postTask()
verfügbar sind. Ohne explizit festgelegte Priorität verhält sich ein scheduler.yield()
in einem scheduler.postTask()
-Callback im Grunde genauso wie im vorherigen Beispiel.
Wenn jedoch eine Priorität festgelegt ist, z. B. eine niedrige 'background'
-Priorität, gilt Folgendes:
async function lowPriorityJob() {
part1();
await scheduler.yield();
part2();
}
scheduler.postTask(lowPriorityJob, {priority: 'background'});
Die Fortsetzung wird mit einer höheren Priorität als andere 'background'
-Aufgaben geplant. Die erwartete priorisierte Fortsetzung wird also vor allen ausstehenden 'background'
-Aufgaben ausgeführt. Sie hat jedoch eine niedrigere Priorität als andere Standard- oder Aufgaben mit hoher Priorität. Es handelt sich weiterhin um 'background'
-Arbeit.
Wenn Sie also eine Aufgabe mit niedriger Priorität mit 'background'
scheduler.postTask()
(oder mit requestIdleCallback
) planen, wartet die Fortsetzung nach einem scheduler.yield()
darin auch, bis die meisten anderen Aufgaben abgeschlossen sind und der Hauptthread zum Ausführen inaktiv ist. Genau das ist das gewünschte Ergebnis, wenn Sie bei einem Job mit niedriger Priorität Yield verwenden.
Verwendung der API
Derzeit ist scheduler.yield()
nur in Chromium-basierten Browsern verfügbar. Wenn Sie sie verwenden möchten, müssen Sie die Funktion erkennen und bei anderen Browsern auf eine sekundäre Methode zum Ausliefern zurückgreifen.
scheduler-polyfill
ist eine kleine Polyfill für scheduler.postTask
und scheduler.yield
, die intern eine Kombination von Methoden verwendet, um einen Großteil der Funktionen der Planungs-APIs in anderen Browsern zu emulieren. Die Prioritätsübernahme von scheduler.yield()
wird jedoch nicht unterstützt.
Wenn Sie eine Polyfill vermeiden möchten, können Sie mit setTimeout()
ausgeben und den Verlust einer priorisierten Fortsetzung akzeptieren oder in nicht unterstützten Browsern nicht ausgeben, wenn dies nicht akzeptabel ist. Weitere Informationen finden Sie in der scheduler.yield()
-Dokumentation unter „Lange Aufgaben optimieren“.
Die wicg-task-scheduling
-Typen können auch für die Typprüfung und IDE-Unterstützung verwendet werden, wenn Sie scheduler.yield()
mithilfe der Funktion „Feature-Erkennung“ erkennen und selbst einen Fallback hinzufügen.
Weitere Informationen
Weitere Informationen zur API und zur Interaktion mit Aufgabenprioritäten und scheduler.postTask()
finden Sie in den MDN-Dokumenten scheduler.yield()
und Priorisierte Aufgabenplanung.
Weitere Informationen zu langen Aufgaben, ihrer Auswirkung auf die Nutzerfreundlichkeit und wie Sie sie optimieren können, finden Sie im Hilfeartikel Lange Aufgaben optimieren.