Przedstawiamy test źródła Scheduler.yield

Tworzenie witryn, które szybko reagują na dane wejściowe użytkowników, było jednym z najtrudniejszych aspektów wydajności stron internetowych. Zespół Chrome dokładał wszelkich starań, aby pomóc deweloperom w rozwiązaniu tego problemu. W tym roku ogłoszono, że dane dotyczące interakcji do kolejnego wyrenderowania (INP) przestaną być eksperymentalne i przejdą do statusu oczekujący. W marcu 2024 r. zastąpi on opóźnienie przy pierwszym działaniu (FID) jako podstawowy wskaźnik internetowy.

W ramach naszych ciągłych wysiłków zmierzających do udostępniania nowych interfejsów API, które pomagają programistom stron internetowych w czynieniu ich jak najszybszymi, zespół Chrome prowadzi obecnie testy wersji próbnej origin dla scheduler.yield, począwszy od wersji 115 Chrome. scheduler.yield to nowa funkcja interfejsu Scheduler API, która umożliwia łatwiejszy i lepszy sposób oddania kontroli nad wątkiem głównemu niż metody tradycyjnie stosowane.

O ustąpieniu

Kod JavaScript do wykonywania zadań stosuje model wykonywania do końca. 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 sterowanie zostaje zwrócone do wątku głównego, co pozwala wątkowi głównemu przetworzyć następne zadanie w kole.

Z wyjątkiem skrajnych przypadków, gdy zadanie nigdy się nie kończy (np. pętla nieskończona), zwracanie jest nieuniknionym aspektem logiki planowania zadań w JavaScript. To się stanie, tylko kwestia czasu. Lepiej wcześniej niż później. Jeśli zadania trwają zbyt długo (czyli dłużej niż 50 ms), są uznawane za długie zadania.

Długie zadania powodują słabą responsywność 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 są wykonywane, tym większe prawdopodobieństwo, że użytkownicy będą mieli wrażenie, że strona działa wolno lub wręcz się zawiesza.

Jednak fakt, że Twój kod uruchamia zadanie w przeglądarce, nie oznacza, że musisz czekać, aż to zadanie się zakończy, zanim przekazanie sterowania do wątku głównego. Możesz poprawić responsywność na dane wprowadzane przez użytkownika na stronie, wyraźnie oddając zadanie, co spowoduje jego przerwanie i dokończenie przy następnej okazji. Dzięki temu inne zadania mogą szybciej uzyskać czas na wątku głównym niż wtedy, gdy musiałyby czekać na zakończenie długich zadań.

Ilustracja pokazująca, jak podział zadania może ułatwić reagowanie na dane wejściowe. U góry długie zadanie blokuje działanie metody obsługi zdarzenia, dopóki nie zostanie ono ukończone. U dołu zadanie podzielone na fragmenty pozwala obsłudze zdarzenia działać szybciej niż normalnie.
Wizualizacja oddania kontroli nad wątkiem głównemu. Na górze zwracanie zasobów występuje tylko po zakończeniu zadania, co oznacza, że zadania mogą zająć więcej czasu, zanim zwrócą kontrolę do wątku głównego. Na dole widać, że rezygnacja jest wykonywana jawnie, dzieląc długie zadanie na kilka krótszych. Dzięki temu interakcje z użytkownikiem mogą być wykonywane wcześniej, co poprawia szybkość reakcji na dane wejściowe i wskaźnik INP.

Gdy wyraźnie zrezygnujesz, mówisz przeglądarce: „Rozumiem, że wykonanie tej pracy może zająć trochę czasu, i nie chcę, abyś musiał wszystko zrobić, zanim zareagujesz na dane wejściowe użytkownika lub inne zadania, które też mogą być ważne”. Jest to cenne narzędzie w arsenalu dewelopera, które może znacznie poprawić wygodę użytkowników.

Problemy z obecnymi strategiami rentowności

Typowa metoda uzyskiwania korzysta z funkcji setTimeout z wartością limitu czasu 0. Działa to, ponieważ funkcja wywołania zwrotnego przekazana do funkcji setTimeout przeniesie pozostałą pracę do osobnego zadania, które zostanie umieszczone w kole do wykonania w późniejszym czasie. Zamiast czekać, aż przeglądarka sama się podda, mówisz: „Podzielmy ten duży kawałek pracy na mniejsze części”.

Jednak przekazanie kontroli za pomocą funkcji setTimeout może mieć niepożądany efekt uboczny: zadania, które pojawiają się po punkcie przekazania kontroli, trafią na koniec kolejki zadań. Zadania zaplanowane przez interakcje z użytkownikiem nadal będą trafiać na początek kolejki, ale pozostałe zadania, które chcesz wykonać po wyraźnym oddaniu kontroli, mogą zostać opóźnione przez inne zadania z konkurencyjnych źródeł, które zostały dodane do kolejki wcześniej.

Aby zobaczyć, jak to działa, wypróbuj demo na Glitch lub eksperymentuj z umieszczoną poniżej wersją. Demo składa się z kilku przycisków, które możesz kliknąć, oraz pola poniżej, w którym zapisywane są informacje o wykonywaniu zadań. Po przejściu na stronę wykonaj te czynności:

  1. Kliknij górny przycisk Uruchamiaj zadania okresowo, aby zaplanować uruchamianie zadań blokujących w określonych odstępach czasu. Po kliknięciu tego przycisku w dzienniku zadań pojawi się kilka komunikatów o treści Wykonano zadanie blokowania z użyciem setInterval.
  2. Następnie kliknij przycisk Wykonaj pętlę, zwracając setTimeout w każdej iteracji.

W polu u dołu strony zobaczysz komunikat 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

Dane wyjściowe pokazują zachowanie „końca kolejki zadań”, które występuje po zwróceniu wartości setTimeout. Wykonywana pętla przetwarza 5 elementów i zwraca wartość setTimeout po przetworzeniu każdego z nich.

Przykład ten ilustruje typowy problem w internecie: nie jest niczym niezwykłym, że skrypt, zwłaszcza zewnętrzny, rejestruje funkcję zegara, która wykonuje pracę w określonym odstępie czasu. Zachowanie „końca kolejki zadań” związane z zrezygnowaniem z użycia funkcji setTimeout oznacza, że zadania z innych źródeł mogą zostać umieszczone w kolejce przed pozostałymi zadaniami, które pętla musi wykonać po zrezygnowaniu z użycia funkcji.

W zależności od aplikacji może to być pożądany efekt, ale w wielu przypadkach może być też powodem, dla którego deweloperzy nie chcą tak łatwo oddawać kontroli nad wątkiem głównym. Oddawanie jest dobre, ponieważ interakcje z użytkownikiem mogą się uruchomić wcześniej, ale pozwala też innym działaniom niewymagającym interakcji z użytkownikiem na uzyskanie czasu na głównym wątku. To prawdziwy problem, ale scheduler.yield może Ci pomóc go rozwiązać.

Wejdź: scheduler.yield

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

Warto zauważyć, że rezygnacja nie była celem projektowania funkcji setTimeout, ale raczej miłym efektem ubocznym zaplanowania wywołania zwrotnego do wykonania w przyszłości, nawet przy określonym czasie oczekiwania 0. Należy jednak pamiętać, że przekazanie kontroli setTimeout spowoduje, że pozostała praca zostanie przeniesiona na koniec kolejki zadań. Domyślnie scheduler.yield przesyła pozostałe zadania do początku kolejki. Oznacza to, że zadania, które chcesz wznowić natychmiast po oddaniu, nie będą odkładane na dalszy plan zadań z innych źródeł (z wyjątkiem interakcji z użytkownikami).

scheduler.yield to funkcja, która zwraca głównemu wątkowi Promise. Oznacza to, że możesz await go użyć w funkcji async:

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

  // Yield!
  await scheduler.yield();

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

Aby zobaczyć, jak działa scheduler.yield:

  1. Wejdź na chrome://flags.
  2. Włącz eksperyment Eksperymentalne funkcje platformy internetowej. Po wykonaniu tej czynności może być konieczne ponowne uruchomienie Chrome.
  3. Otwórz stronę demonstracyjną lub skorzystaj z umieszczonej poniżej wersji demonstracyjnej.
  4. Kliknij górny przycisk Uruchamiaj zadania okresowo.
  5. Na koniec kliknij przycisk Uruchom pętlę, zwracając 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 zwraca wartość za pomocą funkcji setTimeout, możesz zauważyć, że pętla – mimo że zwraca wartość po każdej iteracji – nie wysyła pozostałej pracy na koniec kolejki, ale na jej początek. Dzięki temu możesz połączyć zalety obu rozwiązań: możesz użyć funkcji yield, aby poprawić szybkość działania formularzy na stronie, ale też mieć pewność, że zadanie, które chcesz ukończyć po użyciu tej funkcji, nie zostanie opóźnione.

Wypróbuj tę funkcję!

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

  1. Jeśli chcesz eksperymentować z scheduler.yield lokalnie, wpisz chrome://flags na pasku adresu Chrome i w menu w sekcji Eksperymentalne funkcje platformy internetowej wybierz Włącz. Dzięki temu funkcja scheduler.yield (i wszystkie inne funkcje eksperymentalne) będą dostępne tylko w Twojej instancji Chrome.
  2. Jeśli chcesz włączyć scheduler.yield dla prawdziwych użytkowników Chromium w publicznie dostępnej domenie, musisz zarejestrować się w ramach testowania origin scheduler.yield. Dzięki temu możesz bezpiecznie eksperymentować z proponowanymi funkcjami przez określony czas. Zespół Chrome będzie też mieć cenne informacje o tym, jak te funkcje są używane w praktyce. Więcej informacji o tym, jak działają testy pochodzenia, znajdziesz w tym przewodniku.

Sposób korzystania z tagu scheduler.yield (przy jednoczesnym zachowaniu obsługi w przypadku przeglądarek, które go nie obsługują) zależy od Twoich celów. Możesz użyć oficjalnej polyfill. Rozszerzenie polyfill jest przydatne, jeśli w Twoim przypadku występuje jeden z tych problemów:

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

Jeśli to nie Twój przypadek, polyfill może nie być dla Ciebie odpowiedni. W takim przypadku możesz wdrożyć własne rozwiązanie zastępcze na kilka sposobów. Pierwsze podejście używa funkcji scheduler.yield, jeśli jest dostępne, ale w przeciwnym razie korzysta z funkcji 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 zadziałać, ale jak się zapewne domyślasz, przeglądarki, które nie obsługują scheduler.yield, nie będą działać jako „pierwsze w kolejce”. Jeśli wolisz w ogóle nie uzyskiwać wartości zwracanej, możesz spróbować innego podejścia, które wykorzystuje funkcję scheduler.yield, jeśli jest dostępna, ale nie zwraca wartości zwracanej, jeśli nie jest dostępne:

// 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 ekscytujące uzupełnienie interfejsu Scheduler API, które, jak mamy nadzieję, ułatwi programistom zwiększenie szybkości działania w porównaniu z obecnymi strategiami generowania zysków. Jeśli scheduler.yield wydaje Ci się przydatnym interfejsem API, weź udział w naszych badaniach, aby pomóc nam go ulepszyć, i przekaż opinię na temat tego, jak można go jeszcze bardziej udoskonalić.

Baner powitalny z Unsplash autorstwa Jonathana Allisona.