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 elementy formularzy 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ć, co z kolei pomoże 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 już umieszczonych w kole, co może spowodować długie opóźnienia między przekazaniem wątku głównego a zakoń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 działający 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ć on 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
znajdują się obok siebie, ale na prawdziwej stronie mogą być wywoływane w zupełnie innych miejscach, np. skrypt własny i skrypt zewnętrzny mogą niezależnie od siebie konfigurować zadania do wykonania. Mogą to być też 2 zadania z osobnych komponentów, które są wywoływane głęboko w planiście 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ć:
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
, wykona się w ten sposób:
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 ją funkcji zostanie umieszczona w kolejce o nieco wyższym priorytecie niż inne podobne zadania. Jeśli wartość myJob
zostanie zmieniona na „Użyj”:
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ółpracuje 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 podobny sposób 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 priorytetem wyższym niż w przypadku innych zadań 'background'
(czyli z oczekiwanym priorytetem kontynuacji przed każdym oczekującym zadaniem 'background'
), ale nadal będzie mieć niższy priorytet niż inne zadania domyślne lub zadania o wysokim priorytecie. Wciąż będzie to zadanie '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łączyć się na inny sposób rezygnacji w przypadku innych przeglądarek.
scheduler-polyfill
to mała biblioteka polyfill dla interfejsów scheduler.postTask
i scheduler.yield
, która 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. Zapoznaj się z dokumentacją scheduler.yield()
w sekcji Optymalizowanie długich zadań w celu uzyskania lepszych wyników.
Typów wicg-task-scheduling
można też używać do sprawdzania typu i obsługi IDE, jeśli funkcja wykrywa scheduler.yield()
i samodzielnie dodajesz wersję zapasową.
Więcej informacji
Więcej informacji o interfejsie API i jego interakcji z priorytetami zadań oraz scheduler.postTask()
znajdziesz w dokumentach scheduler.yield()
i Harmonogramowanie 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ń.