Data publikacji: 6 marca 2025 r.
Strona wydaje się powolna i nie reaguje, gdy długie zadania zajmują wątek główny, uniemożliwiając mu wykonywanie innych ważnych czynności, takich jak reagowanie na działania użytkownika. W rezultacie nawet wbudowane elementy sterujące formularza mogą wyglądać na uszkodzone – jakby strona była zamrożona – nie mówiąc już o bardziej złożonych komponentach niestandardowych.
scheduler.yield()
to sposób na przekazanie kontroli wątkowi głównemu, który umożliwia przeglądarce wykonanie oczekujących zadań o wysokim priorytecie, a następnie kontynuowanie działania w miejscu, w którym zostało przerwane. Dzięki temu strona jest bardziej responsywna, co z kolei pomaga poprawić czas od interakcji do kolejnego wyrenderowania (INP).
scheduler.yield
oferuje ergonomiczny interfejs API, który robi dokładnie to, co mówi jego nazwa: wykonanie funkcji, w której jest wywoływany, zostaje wstrzymane w miejscu wyrażenia await scheduler.yield()
i przekazuje sterowanie do wątku głównego, dzieląc zadanie. Wykonanie pozostałej części funkcji, czyli jej kontynuacji, zostanie zaplanowane w ramach nowego zadania pętli zdarzeń.
async function respondToUserClick() {
giveImmediateFeedback();
await scheduler.yield(); // Yield to the main thread.
slowerComputation();
}
Szczególną zaletą scheduler.yield
jest to, że kontynuacja po wywołaniu funkcji yield jest zaplanowana do uruchomienia przed uruchomieniem innych podobnych zadań, które zostały dodane do kolejki przez stronę. Priorytetem jest kontynuowanie zadania, a nie rozpoczynanie nowych.
Do dzielenia zadań można też używać funkcji takich jak setTimeout
czy scheduler.postTask
, ale te kontynuacje zwykle działają po wszystkich nowych zadaniach, które są już w kolejce, co może powodować długie opóźnienia między przekazaniem sterowania do głównego wątku a zakończeniem pracy.
Priorytetowe kontynuacje po przekazaniu sterowania
scheduler.yield
jest częścią interfejsu Prioritized Task Scheduling API. Jako programiści stron internetowych zwykle nie mówimy o kolejności, w jakiej pętla zdarzeń wykonuje zadania, w kategoriach jawnych priorytetów, ale priorytety względne zawsze istnieją, np. wywołanie zwrotne requestIdleCallback
jest wykonywane po wszystkich wywołaniach zwrotnych w kolejcesetTimeout
, a wywołany odbiornik zdarzenia wejściowego zwykle jest wykonywany przed zadaniem umieszczonym w kolejce za pomocą setTimeout(callback, 0)
.
Planowanie zadań z nadawaniem priorytetów sprawia, że jest to bardziej przejrzyste, co ułatwia określenie, które zadanie zostanie wykonane przed innym. Umożliwia też dostosowywanie priorytetów w celu zmiany kolejności wykonywania zadań w razie potrzeby.
Jak już wspomnieliśmy, dalsze wykonywanie funkcji po wywołaniu scheduler.yield()
ma wyższy priorytet niż rozpoczynanie innych zadań. Koncepcja przewodnia polega na tym, że kontynuacja zadania powinna być wykonywana w pierwszej kolejności, przed przejściem do innych zadań. Jeśli zadanie jest dobrze napisanym kodem, który okresowo ustępuje, aby przeglądarka mogła wykonywać inne ważne czynności (np. odpowiadać na dane wejściowe użytkownika), nie powinno być karane za ustępowanie przez przyznawanie mu niższego priorytetu niż innym podobnym zadaniom.
Oto przykład: 2 funkcje w kolejce do uruchomienia w różnych zadaniach za pomocą setTimeout
.
setTimeout(myJob);
setTimeout(someoneElsesJob);
W tym przypadku 2 wywołania setTimeout
znajdują się obok siebie, ale na prawdziwej stronie mogą być wywoływane w zupełnie różnych miejscach, np. skrypt własny i skrypt innej firmy mogą niezależnie od siebie konfigurować działanie, lub mogą to być 2 zadania z osobnych komponentów wywoływane w harmonogramie frameworka.
Oto jak może wyglądać ta praca w Narzędziach deweloperskich:
myJob
jest oznaczona jako długie zadanie, które blokuje przeglądarce możliwość wykonywania innych czynności podczas jego działania. Załóżmy, ż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ż myJobPart2
miało zostać uruchomione z setTimeout
w ramach myJob
, ale to zaplanowane uruchomienie następuje po zaplanowaniu someoneElsesJob
, wykonanie będzie wyglądać tak:
Podzieliliśmy zadanie z setTimeout
, aby przeglądarka mogła reagować w trakcie myJob
, ale teraz druga część myJob
jest wykonywana dopiero po zakończeniu someoneElsesJob
.
W niektórych przypadkach może to być w porządku, ale zwykle nie jest to optymalne rozwiązanie. myJob
przekazywał kontrolę do głównego wątku, aby strona mogła reagować na dane wejściowe użytkownika, a nie po to, aby całkowicie zrezygnować z głównego wątku. Jeśli someoneElsesJob
działa szczególnie wolno lub zaplanowano wiele innych zadań oprócz someoneElsesJob
, może minąć dużo czasu, zanim zostanie uruchomiona druga połowa myJob
. Prawdopodobnie nie było to zamiarem dewelopera, gdy dodawał setTimeout
do myJob
.
Wpisz scheduler.yield()
, co spowoduje umieszczenie kontynuacji każdej funkcji, która ją wywołuje, w kolejce o nieco wyższym priorytecie niż rozpoczęcie innych podobnych zadań. Jeśli myJob
zostanie zmieniony tak, aby z niego korzystać:
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 nadal ma możliwość reagowania, ale teraz kontynuacja zadania myJob
ma wyższy priorytet niż rozpoczęcie nowego zadania someoneElsesJob
, więc zadanie myJob
zostanie ukończone przed rozpoczęciem zadania someoneElsesJob
. Jest to znacznie bliższe oczekiwaniom dotyczącym przekazywania kontroli wątkowi głównemu w celu zachowania responsywności, a nie całkowitego rezygnowania z wątku głównego.
Dziedziczenie priorytetu
W ramach większego interfejsu Prioritized Task Scheduling API scheduler.yield()
dobrze współpracuje z jawnie określonymi priorytetami dostępnymi w scheduler.postTask()
. Jeśli priorytet nie jest ustawiony wprost, wywołanie zwrotne scheduler.yield()
w wywołaniu zwrotnym scheduler.postTask()
będzie działać w zasadzie tak samo jak w poprzednim przykładzie.
Jeśli jednak ustawisz priorytet, np. niski 'background'
:
async function lowPriorityJob() {
part1();
await scheduler.yield();
part2();
}
scheduler.postTask(lowPriorityJob, {priority: 'background'});
Kontynuacja zostanie zaplanowana z priorytetem wyższym niż inne zadania 'background'
– oczekiwana kontynuacja o wyższym priorytecie zostanie wykonana przed wszystkimi oczekującymi zadaniami 'background'
– ale nadal z niższym priorytetem niż inne zadania domyślne lub o wysokim priorytecie. Pozostanie ona zadaniem 'background'
.
Oznacza to, że jeśli zaplanujesz zadanie o niskim priorytecie z użyciem 'background'
scheduler.postTask()
(lub requestIdleCallback
), kontynuacja po scheduler.yield()
w ramach tego zadania również poczeka, aż większość innych zadań zostanie wykonana, a wątek główny będzie bezczynny. Wtedy zostanie uruchomiona, co jest dokładnie tym, czego oczekujesz od przekazywania sterowania w zadaniu o niskim priorytecie.
Jak korzystać z interfejsu API
Obecnie scheduler.yield()
jest dostępna tylko w przeglądarkach opartych na Chromium, więc aby jej używać, musisz wykrywać funkcje i w przypadku innych przeglądarek stosować alternatywny sposób przekazywania sterowania.
scheduler-polyfill
to mały polyfill dla scheduler.postTask
i scheduler.yield
, który wewnętrznie wykorzystuje kombinację metod do emulowania wielu funkcji interfejsów API planowania w innych przeglądarkach (chociaż dziedziczenie priorytetu scheduler.yield()
nie jest obsługiwane).
Jeśli chcesz uniknąć polyfillu, możesz użyć setTimeout()
i zaakceptować utratę priorytetowej kontynuacji lub nawet nie używać tej funkcji w nieobsługiwanych przeglądarkach, jeśli nie jest to dopuszczalne. Więcej informacji znajdziesz w scheduler.yield()
dokumentacji dotyczącej optymalizacji długich zadań.
wicg-task-scheduling
Typy można też wykorzystać do sprawdzania typów i obsługi IDE, jeśli samodzielnie wykrywasz funkcje scheduler.yield()
i dodajesz rezerwowy kod.
Więcej informacji
Więcej informacji o interfejsie API i jego interakcjach z priorytetami zadań oraz scheduler.postTask()
znajdziesz w dokumentach scheduler.yield()
i Prioritized Task Scheduling w MDN.
Więcej informacji o długich zadaniach, ich wpływie na wygodę użytkowników i sposobach ich optymalizacji znajdziesz w artykule Optymalizowanie długich zadań.