scheduler.yield() verwenden, um lange Aufgaben aufzuteilen

Brendan Kenny
Brendan Kenny

Veröffentlicht am 6. März 2025

Browser Support

  • Chrome: 129.
  • Edge: 129.
  • Firefox: 142.
  • Safari: not supported.

Source

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:

Zwei Aufgaben im Leistungsbereich der Chrome-Entwicklertools. Beide werden als lange Aufgaben angegeben. Die Funktion „myJob“ nimmt die gesamte Ausführung der ersten Aufgabe in Anspruch und „someoneElsesJob“ die gesamte Ausführung der zweiten Aufgabe.

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:

Drei Aufgaben im Bereich „Leistung“ der Chrome-Entwicklertools. Zuerst wird die Funktion „myJobPart1“ ausgeführt, dann die lange Aufgabe „someoneElsesJob“ und schließlich die Aufgabe „myJobPart2“.

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:

Zwei Aufgaben im Leistungsbereich der Chrome-Entwicklertools. Beide werden als lange Aufgaben angegeben. Die Funktion „myJob“ nimmt die gesamte Ausführung der ersten Aufgabe in Anspruch und „someoneElsesJob“ die gesamte Ausführung der zweiten Aufgabe.

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.

Weitere Informationen zu langen Tasks