Przedstawiamy test źródła Scheduler.yield

Tworzenie witryn, które szybko reagują na dane wejściowe użytkownika, jest jednym z najtrudniejszych aspektów wydajności internetu. Zespół Chrome intensywnie pracuje nad tym, aby pomóc deweloperom w tym zakresie. W tym roku ogłoszono, że interakcja do kolejnego wyrenderowania (INP) przejdzie ze stanu eksperymentalnego do stanu oczekującego. W marcu 2024 r. zastąpi on opóźnienie przy pierwszym działaniu (FID) jako podstawowy wskaźnik internetowy.

W ramach ciągłych wysiłków na rzecz udostępniania nowych interfejsów API, które pomagają programistom stron internetowych tworzyć jak najszybsze witryny, zespół Chrome prowadzi obecnie wersję próbną origin interfejsu scheduler.yield, która jest dostępna od wersji 115 Chrome. scheduler.yield to proponowany nowy dodatek do interfejsu API harmonogramu, który zapewnia łatwiejszy i lepszy sposób przekazywania kontroli z powrotem do głównego wątku niż metody, na których tradycyjnie polegamy.

Po wygenerowaniu

JavaScript używa modelu „run-to-completion” do obsługi zadań. Oznacza to, że gdy zadanie jest wykonywane w wątku głównym, trwa tak długo, jak jest to konieczne do jego ukończenia. Po zakończeniu zadania kontrola jest przekazywana z powrotem do wątku głównego, co umożliwia mu przetworzenie kolejnego zadania w kolejce.

Oprócz skrajnych przypadków, w których zadanie nigdy się nie kończy (np. pętla nieskończona), przekazywanie sterowania jest nieuniknionym aspektem logiki planowania zadań w JavaScript. To się stanie, tylko nie wiadomo kiedy, a im szybciej, tym lepiej. Gdy zadania trwają zbyt długo – dokładnie ponad 50 milisekund – są uznawane za długie zadania.

Długie zadania są przyczyną słabej responsywności strony, ponieważ opóźniają reakcję przeglądarki na dane wejściowe użytkownika. Im częściej występują długie zadania i im dłużej trwają, tym większe prawdopodobieństwo, że użytkownicy odniosą wrażenie, że strona działa powoli, a nawet że jest całkowicie zepsuta.

Jednak sam fakt, że kod uruchamia zadanie w przeglądarce, nie oznacza, że musisz czekać na jego zakończenie, zanim kontrola zostanie przekazana z powrotem do głównego wątku. Możesz zwiększyć responsywność strony na dane wejściowe użytkownika, jawnie przerywając zadanie, co spowoduje jego dokończenie przy najbliższej okazji. Dzięki temu inne zadania mogą być wykonywane w wątku głównym szybciej niż w przypadku, gdyby musiały czekać na zakończenie długich zadań.

Ilustracja pokazująca, jak podział zadania może poprawić responsywność danych wejściowych. U góry długie zadanie blokuje działanie procedury obsługi zdarzeń do czasu jego zakończenia. U dołu podzielone zadanie umożliwia wcześniejsze uruchomienie procedury obsługi zdarzeń niż w innym przypadku.
Wizualizacja przekazywania kontroli z powrotem do wątku głównego. W przypadku tego podejścia ustępowanie następuje dopiero po zakończeniu zadania, co oznacza, że zadania mogą trwać dłużej, zanim zwrócą kontrolę do wątku głównego. U dołu wyraźnie widać, że zadanie jest dzielone na mniejsze części. Umożliwia to wcześniejsze uruchamianie interakcji użytkownika, co poprawia szybkość reakcji na dane wejściowe i INP.

Gdy jawnie przekazujesz kontrolę, mówisz przeglądarce: „Rozumiem, że praca, którą zamierzam wykonać, może zająć trochę czasu, i nie chcę, abyś musiała wykonać całą tę pracę, zanim odpowiesz na dane wejściowe użytkownika lub inne zadania, które też mogą być ważne”. To cenne narzędzie w zestawie narzędzi dewelopera, które może znacznie poprawić wygodę użytkowników.

Problem z obecnymi strategiami zwiększania przychodów

Popularna metoda przekazywania sterowania używa funkcji setTimeout z wartością limitu czasu 0. Działa to dlatego, że wywołanie zwrotne przekazane do funkcji setTimeout przeniesie pozostałą część pracy do osobnego zadania, które zostanie umieszczone w kolejce do późniejszego wykonania. Zamiast czekać, aż przeglądarka sama się zatrzyma, mówisz: „podziel ten duży blok pracy na mniejsze części”.

Jednak wywołanie funkcji setTimeout może mieć niepożądany efekt uboczny: zadanie, które następuje po punkcie wstrzymania, zostanie przeniesione na koniec kolejki zadań. Zadania zaplanowane w wyniku interakcji użytkownika nadal będą trafiać na początek kolejki, ale pozostała praca, którą chcesz wykonać po wyraźnym przekazaniu sterowania, może zostać opóźniona przez inne zadania z konkurencyjnych źródeł, które zostały umieszczone w kolejce przed nią.

Aby zobaczyć to w praktyce, wypróbuj tę wersję demonstracyjną w Codepen lub poeksperymentuj z nią w tej wersji osadzonej. Wersja demonstracyjna składa się z kilku przycisków, które możesz kliknąć, oraz pola pod nimi, w którym rejestrowane są uruchomienia zadań. Gdy znajdziesz się na stronie, wykonaj te czynności:

  1. Kliknij górny przycisk o nazwie Uruchamiaj zadania okresowo, aby zaplanować uruchamianie zadań blokujących co jakiś czas. Gdy klikniesz ten przycisk, w dzienniku zadań pojawi się kilka wiadomości o treści Uruchomiono zadanie blokujące z setInterval.
  2. Następnie kliknij przycisk Uruchom pętlę, zwracając wartość setTimeout w każdej iteracji.

Zauważysz, że w polu u dołu wersji demonstracyjnej pojawi się tekst podobny do tego:

Processing loop item 1
Processing loop item 2
Ran blocking task via setInterval
Processing loop item 3
Ran blocking task via setInterval
Processing loop item 4
Ran blocking task via setInterval
Processing loop item 5
Ran blocking task via setInterval
Ran blocking task via setInterval

Te dane wyjściowe pokazują zachowanie „końca kolejki zadań”, które występuje podczas przekazywania z użyciem setTimeout. Pętla przetwarza 5 elementów i po każdym z nich zwraca wartość setTimeout.

Ilustruje to typowy problem w internecie: często skrypt, zwłaszcza skrypt zewnętrzny, rejestruje funkcję timera, która wykonuje pracę w określonych odstępach czasu. Zachowanie „końca kolejki zadań” związane z przekazywaniem sterowania za pomocą setTimeout oznacza, że zadania z innych źródeł mogą zostać umieszczone w kolejce przed pozostałymi zadaniami, które pętla musi wykonać po przekazaniu sterowania.

W zależności od aplikacji może to być pożądany lub niepożądany efekt, ale w wielu przypadkach to zachowanie jest powodem, dla którego deweloperzy mogą niechętnie rezygnować z kontroli nad głównym wątkiem. Przekazywanie sterowania jest korzystne, ponieważ interakcje użytkownika mogą być wykonywane wcześniej, ale umożliwia też wykonywanie innych działań niezwiązanych z interakcjami użytkownika w głównym wątku. To prawdziwy problem, ale scheduler.yield może pomóc go rozwiązać.

Wejdź: scheduler.yield

scheduler.yield jest dostępna za flagą jako eksperymentalna funkcja platformy internetowej od wersji 115 Chrome. Możesz się zastanawiać, dlaczego potrzebujesz specjalnej funkcji do przekazywania sterowania, skoro setTimeout już to robi.

Warto zauważyć, że przekazywanie sterowania nie było celem projektowym setTimeout, ale raczej miłym efektem ubocznym planowania wywołania zwrotnego do uruchomienia w późniejszym czasie – nawet przy określonej wartości limitu czasu 0. Ważniejsze jest jednak to, że wywołanie funkcji setTimeout powoduje przeniesienie pozostałej części pracy na koniec kolejki zadań. Domyślnie polecenie scheduler.yield wysyła pozostałe zadania na początek kolejki. Oznacza to, że praca, którą chcesz wznowić natychmiast po przerwaniu, nie zostanie zepchnięta na dalszy plan przez zadania z innych źródeł (z wyjątkiem interakcji użytkownika).

scheduler.yield to funkcja, która przekazuje sterowanie do głównego wątku i zwraca wartość Promise po wywołaniu. Oznacza to, że możesz await w funkcji async:

async function yieldy () {
  // Do some work...
  // ...

  // Yield!
  await scheduler.yield();

  // Do some more work...
  // ...
}

Aby zobaczyć działanie funkcji scheduler.yield:

  1. Wejdź na chrome://flags.
  2. Włącz eksperyment Experimental Web Platform features (Funkcje eksperymentalnej platformy internetowej). Po wykonaniu tej czynności może być konieczne ponowne uruchomienie Chrome.
  3. Otwórz stronę demonstracyjną lub skorzystaj z jej wersji umieszczonej poniżej tej listy.
  4. Kliknij przycisk u góry oznaczony jako Uruchamiaj zadania okresowo.
  5. Na koniec kliknij przycisk Run loop, yielding with scheduler.yield on each iteration (Uruchom pętlę, z scheduler.yield w każdej iteracji).

Dane wyjściowe w polu u dołu strony będą wyglądać mniej więcej tak:

Processing loop item 1
Processing loop item 2
Processing loop item 3
Processing loop item 4
Processing loop item 5
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval

W przeciwieństwie do wersji demonstracyjnej, która używa setTimeout, widać, że pętla – mimo że po każdej iteracji zwraca wartość – nie wysyła pozostałej pracy na koniec kolejki, ale na jej początek. Dzięki temu możesz uzyskać najlepsze z obu rozwiązań: możesz ustąpić, aby poprawić szybkość reakcji na dane wejściowe w witrynie, ale też mieć pewność, że praca, którą chcesz wykonać po ustąpieniu, nie zostanie opóźniona.

Wypróbuj tę funkcję!

Jeśli scheduler.yield Cię zainteresuje i chcesz ją wypróbować, możesz to zrobić na 2 sposoby, począwszy od Chrome w wersji 115:

  1. Jeśli chcesz eksperymentować z scheduler.yield lokalnie, wpisz i zatwierdź chrome://flags na pasku adresu Chrome, a następnie w sekcji Eksperymentalne funkcje platformy internetowej wybierz Włącz. Spowoduje to, że funkcja scheduler.yield (i inne funkcje eksperymentalne) będzie dostępna tylko w Twojej instancji Chrome.
  2. Jeśli chcesz włączyć scheduler.yield dla prawdziwych użytkowników Chromium w publicznie dostępnym źródle, musisz zarejestrować się w scheduler.yield testowaniu origin. Dzięki temu możesz bezpiecznie testować proponowane funkcje przez określony czas, a zespół Chrome zyskuje cenne informacje o tym, jak te funkcje są używane w praktyce. Więcej informacji o tym, jak działają testy origin, znajdziesz w tym przewodniku.

Sposób użycia tagu scheduler.yield przy jednoczesnym zachowaniu obsługi przeglądarek, które go nie implementują, zależy od Twoich celów. Możesz użyć oficjalnego kodu polyfill. Polyfill jest przydatny, jeśli:

  1. W aplikacji używasz już usługi scheduler.postTask do planowania zadań.
  2. Chcesz mieć możliwość ustawiania priorytetów zadań i uzyskiwania przychodów.
  3. Chcesz mieć możliwość anulowania zadań lub zmiany ich priorytetu za pomocą klasy TaskController oferowanej przez interfejs API scheduler.postTask.

Jeśli ta sytuacja Cię nie dotyczy, polyfill może nie być dla Ciebie odpowiedni. W takiej sytuacji możesz samodzielnie utworzyć wersję zapasową na kilka sposobów. Pierwsze podejście używa metody scheduler.yield, jeśli jest dostępna, ale w przeciwnym razie wraca do metody setTimeout:

// A function for shimming scheduler.yield and setTimeout:
function yieldToMain () {
  // Use scheduler.yield if it exists:
  if ('scheduler' in window && 'yield' in scheduler) {
    return scheduler.yield();
  }

  // Fall back to setTimeout:
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

// Example usage:
async function doWork () {
  // Do some work:
  // ...

  await yieldToMain();

  // Do some other work:
  // ...
}

Może to działać, ale jak można się domyślać, przeglądarki, które nie obsługują scheduler.yield, będą zwracać wartość bez zachowania „pierwszeństwa w kolejce”. Jeśli wolisz w ogóle nie rezygnować z wyświetlania reklam, możesz spróbować innego podejścia, które wykorzystuje scheduler.yield, jeśli jest dostępne, ale w przeciwnym razie w ogóle nie rezygnuje z wyświetlania reklam:

// A function for shimming scheduler.yield with no fallback:
function yieldToMain () {
  // Use scheduler.yield if it exists:
  if ('scheduler' in window && 'yield' in scheduler) {
    return scheduler.yield();
  }

  // Fall back to nothing:
  return;
}

// Example usage:
async function doWork () {
  // Do some work:
  // ...

  await yieldToMain();

  // Do some other work:
  // ...
}

scheduler.yield to interesujący dodatek do interfejsu API harmonogramu, który powinien ułatwić programistom zwiększanie responsywności w porównaniu z obecnymi strategiami przekazywania sterowania. Jeśli interfejs scheduler.yield wydaje Ci się przydatny, weź udział w naszych badaniach, aby pomóc nam go ulepszyć, i prześlij opinię na temat tego, jak można go jeszcze bardziej udoskonalić.

Baner powitalny z Unsplash, autor: Jonathan Allison.