Data publikacji: 6 marca 2025 r.
Strona działa wolno i nie odpowiada, gdy długie zadania zajmują główny wątek, uniemożliwiając mu wykonywanie innych ważnych zadań, np. reagowanie na dane wejściowe użytkownika. W rezultacie nawet wbudowane formanty mogą się wydawać użytkownikom uszkodzone, jakby strona była zablokowana, nie mówiąc już o bardziej złożonych komponentach niestandardowych.
scheduler.yield()
to sposób na przekazanie wątku głównemu możliwości wykonania oczekujących zadań o wysokim priorytecie, a następnie kontynuowanie wykonywania kodu od miejsca, w którym zostało ono przerwane. Dzięki temu strona będzie szybciej reagować na działania użytkownika, co z kolei pomaga poprawić czas od interakcji do kolejnego wyrenderowania (INP).
scheduler.yield
udostępnia ergonomiczny interfejs API, który robi dokładnie to, co mówi: wykonywanie funkcji, w której jest wywoływany, jest wstrzymywane w wyrazeniu await scheduler.yield()
i przekazywane do wątku głównego, co umożliwia podzielenie zadania. Wykonywanie pozostałej części funkcji (tzw. kontynuacji funkcji) zostanie zaplanowane w ramach nowego zadania pętli zdarzeń.
async function respondToUserClick() {
giveImmediateFeedback();
await scheduler.yield(); // Yield to the main thread.
slowerComputation();
}
Korzyścią z użycia scheduler.yield
jest to, że kontynuacja po użyciu yield jest zaplanowana do wykonania przed wykonaniem innych podobnych zadań, które zostały dodane do kolejki przez stronę. Priorytetem jest kontynuowanie bieżącego zadania, a nie rozpoczynanie nowych.
Do dzielenia zadań można też używać funkcji takich jak setTimeout
lub scheduler.postTask
, ale te kontynuacje są zwykle wykonywane po wszystkich nowych zadaniach, które są już w kole, co może spowodować długie opóźnienia między przekazaniem wątku głównego a ukończeniem pracy.
Priorytetowe kontynuacje po oddaniu
scheduler.yield
jest częścią interfejsu Prioritized Task Scheduling API. Jako programiści stron internetowych nie mówimy zwykle o kolejności, w jakiej pętla zdarzeń wykonuje zadania pod względem ich wyraźnych priorytetów, ale względne priorytety zawsze istnieją, np. wywołanie zwrotne requestIdleCallback
jest wykonywane po wszystkich oczekujących wywołaniach zwrotnych setTimeout
lub wywołanie zwrotne odbiornika zdarzenia wejściowego jest zwykle wykonywane przed zadaniem oczekującym z setTimeout(callback, 0)
.
Priorytetowe harmonogramowanie zadań sprawia, że jest to bardziej wyraźne, ułatwia ustalanie, które zadanie będzie wykonywane przed innym, oraz umożliwia dostosowywanie priorytetów, aby w razie potrzeby zmienić kolejność wykonywania zadań.
Jak już wspomnieliśmy, dalsze wykonywanie funkcji po zwróceniu wartości przez scheduler.yield()
ma wyższy priorytet niż uruchamianie innych zadań. Zasada jest taka, że kontynuacja zadania powinna być wykonywana jako pierwsza, zanim przejdziesz do innych zadań. Jeśli zadanie to dobrze zachowujący się kod, który okresowo zwalnia procesor, aby przeglądarka mogła wykonywać inne ważne czynności (np. reagować na dane wejściowe użytkownika), nie powinien być karany za zwalnianie przez nadawanie mu niższego priorytetu w porównaniu z innymi podobnymi zadaniami.
Oto przykład: 2 funkcje, które są ustawione w kolejce do wykonania w różnych zadaniach za pomocą funkcji setTimeout
.
setTimeout(myJob);
setTimeout(someoneElsesJob);
W tym przypadku oba wywołania setTimeout
są obok siebie, ale na prawdziwej stronie mogą być wywoływane w zupełnie różnych miejscach, np. skrypt własny i skrypt zewnętrzny mogą niezależnie od siebie konfigurować wykonywane zadania. Mogą to być też 2 zadania z osobnych komponentów, które są wywoływane głęboko w plani programowym frameworka.
Oto, jak może wyglądać taka praca w DevTools:
myJob
jest oznaczone jako długie zadanie, które blokuje przeglądarkę przed wykonywaniem innych czynności. Zakładając, że pochodzi on ze skryptu własnego, możemy go podzielić na:
function myJob() {
// Run part 1.
myJobPart1();
// Yield with setTimeout() to break up long task, then run part2.
setTimeout(myJobPart2, 0);
}
Ponieważ zadanie myJobPart2
zostało zaplanowane do wykonania z zadaniem setTimeout
w ramach zadania myJob
, ale to zaplanowanie zostanie wykonane po zaplanowaniu zadania someoneElsesJob
, wykonanie będzie wyglądać tak:
Zadanie zostało podzielone na setTimeout
, aby przeglądarka mogła reagować na działania w trakcie wykonywania zadania myJob
. Teraz druga część zadania myJob
jest wykonywana dopiero po zakończeniu zadania someoneElsesJob
.
W niektórych przypadkach może to być odpowiednie, ale zwykle nie jest to optymalne rozwiązanie. myJob
oddawał kontrolę głównemu wątkowi, aby zapewnić stronie możliwość reagowania na dane wejściowe użytkownika, ale nie chodziło o to, aby całkowicie zrezygnować z głównego wątku. W przypadku, gdy someoneElsesJob
jest szczególnie powolne lub gdy oprócz someoneElsesJob
zaplanowano też wiele innych zadań, może minąć dużo czasu, zanim zostanie uruchomiona druga połowa myJob
. Prawdopodobnie nie było to zamierzone, gdy deweloper dodał ten element setTimeout
do myJob
.
Wpisz scheduler.yield()
, co spowoduje, że kontynuacja dowolnej funkcji wywołującej tę funkcję będzie miała nieco wyższy priorytet niż inne podobne zadania. Jeśli myJob
zostanie zmieniony, aby używać go:
async function myJob() {
// Run part 1.
myJobPart1();
// Yield with scheduler.yield() to break up long task, then run part2.
await scheduler.yield();
myJobPart2();
}
Teraz wykonanie wygląda tak:
Przeglądarka może nadal być responsywna, ale teraz kontynuowanie zadania myJob
ma wyższy priorytet niż rozpoczęcie nowego zadania someoneElsesJob
, więc zadanie myJob
zostanie ukończone, zanim rozpocznie się zadanie someoneElsesJob
. Jest to znacznie bliższe oczekiwaniom, ponieważ pozwala zachować responsywność wątku głównego, a nie całkowicie z niego rezygnować.
Dziedziczenie priorytetów
Jako część większego interfejsu API do planowania zadań z uwzględnieniem priorytetów interfejs scheduler.yield()
dobrze współdziała z wyraźnymi priorytetami dostępnymi w interfejsie scheduler.postTask()
. Bez jawnie ustawionego priorytetu wywołanie zwrotne scheduler.yield()
w ramach funkcji scheduler.postTask()
będzie działać w podstawie tak samo jak w poprzednim przykładzie.
Jeśli jednak ustawisz priorytet, np. niski priorytet 'background'
:
async function lowPriorityJob() {
part1();
await scheduler.yield();
part2();
}
scheduler.postTask(lowPriorityJob, {priority: 'background'});
Kontynuacja zostanie zaplanowana z wyższym priorytetem niż inne zadania 'background'
, co oznacza, że będzie ona realizowana przed zadaniami o priorytecie 'background'
, ale nadal będzie miała niższy priorytet niż inne zadania domyślne lub zadania o wysokim priorytecie.'background'
Oznacza to, że jeśli zaplanowasz zadanie o niskim priorytecie z użyciem 'background'
scheduler.postTask()
(lub requestIdleCallback
), kontynuacja po scheduler.yield()
w ramach tego zadania będzie też czekać, aż większość innych zadań zostanie ukończona, a wątek główny będzie gotowy do uruchomienia. Jest to dokładnie to, czego oczekujesz od funkcji yielding w przypadku zadania o niskim priorytecie.
Jak korzystać z interfejsu API
Obecnie scheduler.yield()
jest dostępna tylko w przeglądarkach opartych na Chromium, więc aby z niej korzystać, musisz użyć funkcji wykrywania i przełączenia na inny sposób rezygnacji w przypadku innych przeglądarek.
scheduler-polyfill
to mały polyfill dla interfejsów scheduler.postTask
i scheduler.yield
, który wewnętrznie używa kombinacji metod do emulowania wielu funkcji interfejsów API do planowania w innych przeglądarkach (chociaż dziedziczenie priorytetu scheduler.yield()
nie jest obsługiwane).
Jeśli chcesz uniknąć polyfill, możesz użyć instrukcji yield za pomocą setTimeout()
i zaakceptować utratę priorytetowego kontynuowania lub nawet nie stosować yield w nieobsługiwanych przeglądarkach, jeśli nie jest to do przyjęcia. Więcej informacji znajdziesz w dokumentacji scheduler.yield()
na temat optymalizacji długich zadań w celu zwiększenia wydajności.
Typów wicg-task-scheduling
można też używać do sprawdzania typów i obsługi IDE, jeśli funkcja wykrywa scheduler.yield()
i samodzielnie dodajesz alternatywne działanie.
Więcej informacji
Więcej informacji o interfejsie API i jego interakcji z priorytetami zadań oraz scheduler.postTask()
znajdziesz w dokumentach scheduler.yield()
i Planowanie zadań z uwzględnieniem priorytetów na stronie MDN.
Więcej informacji o długich zadaniach, ich wpływie na komfort użytkownika i o tym, co można z nimi zrobić, znajdziesz w artykule Optymalizacja długich zadań.