Veröffentlicht am 6. März 2025
Eine Seite fühlt sich träge und nicht reaktionsschnell an, wenn lange Aufgaben den Hauptthread so lange beschäftigen, dass er keine anderen wichtigen Aufgaben wie das Reagieren auf Nutzereingaben ausführen kann. Daher können selbst integrierte Formularsteuerelemente für Nutzer fehlerhaft aussehen – als ob die Seite eingefroren wäre –, ganz zu schweigen von komplexeren benutzerdefinierten Komponenten.
scheduler.yield()
ist eine Möglichkeit, den Hauptthread freizugeben, damit der Browser alle anstehenden Arbeiten mit hoher Priorität ausführen kann. Anschließend wird die Ausführung an der Stelle fortgesetzt, an der sie unterbrochen wurde. Dadurch bleibt eine Seite reaktionsschneller und die Interaktion bis zum nächsten Rendern (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 am await scheduler.yield()
-Ausdruck angehalten und an den Hauptthread übergeben, wodurch die Aufgabe unterbrochen wird. Die Ausführung des Rests der Funktion, der als Fortsetzung der Funktion bezeichnet wird, wird für die Ausführung in einer neuen Event-Loop-Aufgabe 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 wird, die von der Seite in die Warteschlange gestellt wurden. Die Fortsetzung einer Aufgabe hat Vorrang vor dem Start 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 die Warteschlange eingereihten neuen Aufgaben ausgeführt, was zu langen Verzögerungen zwischen dem Übergeben des Hauptthreads und dem Abschluss der Arbeit führen kann.
Priorisierte Fortsetzungen nach dem Yielding
scheduler.yield
ist Teil der Prioritized Task Scheduling API. Als Webentwickler sprechen wir in der Regel nicht über die Reihenfolge, in der die Ereignisschleife Aufgaben ausführt, in Bezug auf explizite Prioritäten. Die relativen Prioritäten sind jedoch immer vorhanden, z. B. wird ein requestIdleCallback
-Callback nach allen in die Warteschlange gestellten setTimeout
-Callbacks ausgeführt oder ein ausgelöster Eingabeereignis-Listener wird in der Regel vor einer mit setTimeout(callback, 0)
in die Warteschlange gestellten Aufgabe ausgeführt.
Die Priorisierung von Aufgaben macht dies nur expliziter. So lässt sich leichter herausfinden, welche Aufgabe vor einer anderen ausgeführt wird. Außerdem können Sie die Prioritäten anpassen, um die Ausführungsreihenfolge bei Bedarf 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 grundlegende Konzept ist, dass die Fortsetzung einer Aufgabe zuerst ausgeführt werden sollte, bevor mit anderen Aufgaben fortgefahren wird. Wenn es sich bei der Aufgabe um gut funktionierenden Code handelt, der regelmäßig nachgibt, damit der Browser andere wichtige Dinge erledigen kann (z. B. auf Nutzereingaben reagieren), sollte er nicht dafür bestraft werden, dass er nachgibt, indem er nach anderen ähnlichen Aufgaben priorisiert wird.
Hier ist ein Beispiel: Zwei Funktionen, die in verschiedenen Aufgaben mit setTimeout
in die Warteschlange gestellt werden.
setTimeout(myJob);
setTimeout(someoneElsesJob);
In diesem Fall stehen die beiden setTimeout
-Aufrufe direkt nebeneinander. Auf einer echten Seite könnten sie jedoch an ganz unterschiedlichen Stellen aufgerufen werden, z. B. in einem Erstanbieter- und einem Drittanbieter-Script, die unabhängig voneinander Aufgaben einrichten. Es könnten auch zwei Aufgaben aus separaten Komponenten sein, die tief im Scheduler Ihres Frameworks ausgelöst werden.
So könnte das in DevTools aussehen:
myJob
wird als langer Task gekennzeichnet, der den Browser daran hindert, während der Ausführung andere Aufgaben zu erledigen. Angenommen, es stammt aus einem Erstanbieter-Script, können wir es aufschlüsseln:
function myJob() {
// Run part 1.
myJobPart1();
// Yield with setTimeout() to break up long task, then run part2.
setTimeout(myJobPart2, 0);
}
Da myJobPart2
für die Ausführung mit setTimeout
innerhalb von myJob
geplant war, diese Planung jedoch nach der Planung von someoneElsesJob
erfolgt, sieht die Ausführung so aus:
Wir haben die Aufgabe mit setTimeout
aufgeteilt, damit der Browser während der Mitte von myJob
reagieren kann. Der zweite Teil von myJob
wird jedoch erst ausgeführt, nachdem someoneElsesJob
abgeschlossen ist.
In einigen Fällen ist das in Ordnung, aber in der Regel ist es nicht optimal. myJob
hat den Hauptthread freigegeben, damit die Seite weiterhin auf Nutzereingaben reagieren kann, nicht, um den Hauptthread vollständig aufzugeben. Wenn someoneElsesJob
besonders langsam ist oder neben someoneElsesJob
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
hinzugefügt hat.
Geben Sie scheduler.yield()
ein. Dadurch wird die Fortsetzung aller Funktionen, die sie aufrufen, in eine Warteschlange mit etwas höherer Priorität als der Start anderer ähnlicher Aufgaben gestellt. Wenn myJob
geändert wird, um sie zu verwenden:
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 hat weiterhin die Möglichkeit, zu reagieren. Die Fortsetzung der Aufgabe myJob
hat jetzt jedoch Vorrang vor dem Start der neuen Aufgabe someoneElsesJob
. myJob
wird also abgeschlossen, bevor someoneElsesJob
beginnt. Das entspricht viel eher der Erwartung, dass der Hauptthread für die Aufrechterhaltung der Reaktionsfähigkeit freigegeben wird, nicht aber vollständig aufgegeben wird.
Prioritätsübernahme
Als Teil der Prioritized Task Scheduling API scheduler.yield()
lässt sich gut mit den expliziten Prioritäten in scheduler.postTask()
kombinieren. Wenn keine Priorität explizit festgelegt ist, 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:
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. So wird die erwartete priorisierte Fortsetzung vor allen ausstehenden 'background'
-Aufgaben ausgeführt. Die Priorität ist jedoch niedriger als bei anderen Standard- oder Aufgaben mit hoher Priorität. Es bleibt eine 'background'
-Aufgabe.
Wenn Sie also Arbeiten mit niedriger Priorität mit 'background'
scheduler.postTask()
(oder mit requestIdleCallback
) planen, wird die Fortsetzung nach einem scheduler.yield()
auch erst ausgeführt, wenn die meisten anderen Aufgaben abgeschlossen sind und der Hauptthread im Leerlauf ist. Das ist genau das, was Sie von der Übergabe in einem Job mit niedriger Priorität erwarten.
Verwendung der API
Derzeit ist scheduler.yield()
nur in Chromium-basierten Browsern verfügbar. Wenn Sie es verwenden möchten, müssen Sie also eine sekundäre Methode zum Yielding für andere Browser erkennen und darauf zurückgreifen.
scheduler-polyfill
ist ein kleines Polyfill für scheduler.postTask
und scheduler.yield
, das intern eine Kombination aus Methoden verwendet, um viele der Funktionen der Scheduling APIs in anderen Browsern zu emulieren (die scheduler.yield()
-Prioritätsvererbung wird jedoch nicht unterstützt).
Wenn Sie ein Polyfill vermeiden möchten, können Sie mit setTimeout()
yielden und den Verlust einer priorisierten Fortsetzung in Kauf nehmen. In nicht unterstützten Browsern können Sie auch nicht yielden, wenn das nicht akzeptabel ist. Weitere Informationen finden Sie in der Dokumentation zu scheduler.yield()
unter „Lange Aufgaben optimieren“.
Die wicg-task-scheduling
-Typen können auch verwendet werden, um Typüberprüfung und IDE-Unterstützung zu erhalten, wenn Sie scheduler.yield()
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 der scheduler.yield()
- und Prioritized Task Scheduling-Dokumentation auf MDN.