scheduler.yield() verwenden, um lange Aufgaben aufzuteilen

Veröffentlicht: 6. März 2025

Browser Support

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

Source

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:

Zwei Aufgaben, die im Chrome-Entwicklertools-Steuerfeld „Leistung“ angezeigt werden. Beide Aufgaben werden als lang angegeben, wobei die Funktion „myJob“ die gesamte Ausführung der ersten Aufgabe und „someoneElsesJob“ die gesamte Ausführung der zweiten Aufgabe umfasst.

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:

Drei Aufgaben, die im Chrome DevTools-Steuerfeld „Leistung“ angezeigt werden. Die erste Aufgabe führt die Funktion „myJobPart1“ aus, die zweite ist eine lange Aufgabe, die „someoneElsesJob“ ausführt, und die dritte Aufgabe führt „myJobPart2“ 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:

Zwei Aufgaben, die im Chrome-Entwicklertools-Steuerfeld „Leistung“ angezeigt werden. Beide Aufgaben werden als lang angegeben, wobei die Funktion „myJob“ die gesamte Ausführung der ersten Aufgabe und „someoneElsesJob“ die gesamte Ausführung der zweiten Aufgabe umfasst.

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.