Lepsze planowanie JS za pomocą isInputPending()

Nowy interfejs JavaScript API, który może pomóc uniknąć kompromisu między wydajnością wczytywania a odpowiadaniem na dane wejściowe.

Nate Schloss
Nate Schloss
Andrew Comminos
Andrew Comminos

Szybkie wczytywanie jest trudne. Strony, które korzystają z JS do renderowania treści, muszą obecnie dokonywać kompromisu między wydajnością wczytywania a odpornością na dane wejściowe: albo wykonują wszystkie czynności potrzebne do wyświetlenia naraz (lepsza wydajność wczytywania, gorsza reakcja na dane wejściowe), albo dzielą pracę na mniejsze zadania, aby pozostać w stanie reagowania na dane wejściowe i wyświetlanie (gorsza wydajność wczytywania, lepsza reakcja na dane wejściowe).

Aby uniknąć konieczności dokonania takiego kompromisu, Facebook zaproponował i wdrożył w Chromium interfejs API isInputPending(), aby poprawić szybkość reakcji bez uszczerbku na wydajności. Na podstawie opinii z testowania origin wprowadziliśmy kilka aktualizacji interfejsu API. Z przyjemnością informujemy, że jest on teraz domyślnie dostępny w Chromium 87.

Zgodność z przeglądarką

Obsługa przeglądarek

  • Chrome: 87.
  • Edge: 87.
  • Firefox: nieobsługiwane.
  • Safari: nieobsługiwane.

Źródło

isInputPending() jest dostępne w przeglądarkach opartych na Chromium od wersji 87. Żadna inna przeglądarka nie zasygnalizowała zamiaru wysłania interfejsu API.

Tło

Większość pracy w obecnym ekosystemie JS wykonuje się w jednym wątku, czyli wątku głównym. Zapewnia to programistom solidny model wykonywania, ale jeśli skrypt działa przez dłuższy czas, może to negatywnie wpłynąć na wrażenia użytkownika (w szczególności na reagowanie). Jeśli podczas wywoływania zdarzenia wejściowego strona wykonuje dużo operacji, nie będzie obsługiwać zdarzenia wejściowego kliknięcia, dopóki nie zakończy wykonywania tych operacji.

Obecnie sprawdzoną metodą jest podzielenie kodu JavaScript na mniejsze bloki. Podczas wczytywania strony może ona wykonać trochę kodu JavaScript, a potem zwrócić kontrolę przeglądarce. Następnie przeglądarka może sprawdzić kolejkę zdarzeń wejściowych, aby sprawdzić, czy ma coś do przekazania stronie. Potem przeglądarka może wrócić do uruchamiania dodawanych bloków JavaScriptu. To pomaga, ale może powodować inne problemy.

Za każdym razem, gdy strona zwraca kontrolę przeglądarce, ta musi sprawdzić kolejkę zdarzeń wejściowych, przetworzyć zdarzenia i wybrać kolejny blok kodu JavaScript. Przeglądarka szybciej reaguje na zdarzenia, ale ogólny czas wczytywania strony wydłuża się. A jeśli generujemy zbyt częste reklamy, strona wczytuje się zbyt wolno. Jeśli Yield jest używany rzadziej, przeglądarka potrzebuje więcej czasu na reakcję na zdarzenia użytkownika, co może powodować frustrację. Niefajnie.

Diagram pokazujący, że podczas wykonywania długich zadań JS przeglądarka ma mniej czasu na wysyłanie zdarzeń.

W Facebooku chcieliśmy sprawdzić, jak wyglądałoby nowe podejście do wczytywania, które wyeliminowałoby ten frustrujący kompromis. Skontaktowaliśmy się z naszą grupą przyjaciół z Chrome i przedstawiliśmy propozycję isInputPending(). Interfejs isInputPending() API jako pierwszy stosuje koncepcję przerw w przypadku danych wejściowych użytkownika w internecie. Umożliwia on JavaScriptowi sprawdzanie danych wejściowych bez zwracania się do przeglądarki.

Diagram pokazujący, że funkcja isInputPending() pozwala JS sprawdzić, czy użytkownik wprowadził dane, bez całkowitego przekazania wykonania z powrotem do przeglądarki.

Ponieważ interfejs API wzbudził zainteresowanie, nawiązaliśmy współpracę z zespołem Chrome, aby wdrożyć i wprowadzić tę funkcję w Chromium. Dzięki pomocy inżynierów z zespołu Chrome udało nam się wprowadzić poprawki w ramach testów origin (to sposób na przetestowanie zmian i uzyskanie opinii od programistów przed pełnym udostępnieniem interfejsu API).

Wzięliśmy pod uwagę opinie z testów wersji źródłowej i od innych członków grupy roboczej W3C ds. wydajności stron internetowych i wprowadziliśmy zmiany w interfejsie API.

Przykład: harmonogramistka automatyzacji

Załóżmy, że masz do załadowania wiele zadań blokujących wyświetlanie strony, np. generowanie znaczników z komponentów, uwzględnianie elementów początkowych lub po prostu rysowanie fajnego wskaźnika wczytywania. Każdy z nich jest podzielony na osobne zadanie. Korzystając ze wzorca algorytmu szeregowania, naszkicujmy, jak możemy przetwarzać naszą pracę w hipotetycznej funkcji processWorkQueue():

const DEADLINE = performance.now() + QUANTUM;
while (workQueue.length > 0) {
 
if (performance.now() >= DEADLINE) {
   
// Yield the event loop if we're out of time.
    setTimeout
(processWorkQueue);
   
return;
 
}
  let job
= workQueue.shift();
  job
.execute();
}

Wywołując processWorkQueue() później w ramach nowego makrozadania za pomocą setTimeout(), dajemy przeglądarce możliwość pozostania w pewnym stopniu wrażliwą na dane wejściowe (może uruchamiać przetwarzacze zdarzeń przed wznowieniem pracy), a zarazem nadal działać stosunkowo niezakłóconym trybem. Możemy jednak na dłuższy czas zostać odwołani ze względu na inne zadania, które wymagają kontroli nad pętlą zdarzeń lub wydłużają czas oczekiwania o QUANTUM milisekundy.

Nie szkodzi, ale czy możemy zrobić to lepiej? Pewnie!

const DEADLINE = performance.now() + QUANTUM;
while (workQueue.length > 0) {
 
if (navigator.scheduling.isInputPending() || performance.now() >= DEADLINE) {
   
// Yield if we have to handle an input event, or we're out of time.
    setTimeout
(processWorkQueue);
   
return;
 
}
  let job
= workQueue.shift();
  job
.execute();
}

Dzięki wprowadzeniu wywołania navigator.scheduling.isInputPending() możemy szybciej reagować na dane wejściowe, jednocześnie zapewniając, że blokowanie wyświetlania będzie działać bez zakłóceń. Jeśli do czasu zakończenia pracy nie interesuje nas nic poza danymi wejściowymi (np. malowaniem), możemy wygodnie zwiększyć długość QUANTUM.

Domyślnie z poziomu isInputPending() nie są zwracane „ciągłe” zdarzenia. Obejmują one mousemove, pointermove i inne. Jeśli chcesz na nich zarabiać, nie ma problemu. Przekazując obiekt do isInputPending() z wartością includeContinuous true, wszystko jest gotowe:

const DEADLINE = performance.now() + QUANTUM;
const options = { includeContinuous: true };
while (workQueue.length > 0) {
 
if (navigator.scheduling.isInputPending(options) || performance.now() >= DEADLINE) {
   
// Yield if we have to handle an input event (any of them!), or we're out of time.
    setTimeout
(processWorkQueue);
   
return;
 
}
  let job
= workQueue.shift();
  job
.execute();
}

Znakomicie. Platformy takie jak React wzbogacą swoje podstawowe biblioteki harmonogramów o obsługę isInputPending(), korzystając z podobnej logiki. Mamy nadzieję, że dzięki temu deweloperzy, którzy korzystają z tych platform, będą mogli korzystać z funkcji isInputPending() bez konieczności wprowadzania znaczących zmian.

Wydajność nie zawsze jest zła

Warto pamiętać, że w niektórych przypadkach mniejsza wydajność nie jest najlepszym rozwiązaniem. Przekazanie kontroli przeglądarce może być konieczne z wielu powodów innych niż przetwarzanie zdarzeń wprowadzania danych, np. w celu renderowania i wykonania innych skryptów na stronie.

Czasami przeglądarka nie może prawidłowo przypisać oczekujących zdarzeń związanych z wejściem. W szczególności ustawienie złożonych klipów i masek dla elementów iframe z innych domen może generować wyniki fałszywie negatywne (tj. isInputPending() może nieoczekiwanie zwracać wartość fałsz podczas kierowania na te ramki). Jeśli Twoja witryna wymaga interakcji ze stylizowanymi podramkami, upewnij się, że yielding jest wystarczająco częste.

Zwróć też uwagę na inne strony, które mają ten sam cykl zdarzeń. Na platformach takich jak Chrome na Androida dość często zdarza się, że wiele źródeł korzysta z pętli zdarzeń. isInputPending() nigdy nie zwróci wartości true, jeśli dane wejściowe są wysyłane do ramki w innej domenie, dlatego strony w tle mogą zakłócać responsywność stron na pierwszym planie. Możesz częściej zmniejszać, opóźniać lub oddawać uprawnienia, gdy wykonujesz zadania w tle za pomocą interfejsu API Widoczność strony.

Zachęcamy do ostrożnego korzystania z funkcji isInputPending(). Jeśli nie wykona żadnej pracy dotyczącej blokowania użytkowników, prosimy o wyrozumiałość dla innych w pętli zdarzeń, zwiększając częstotliwość generowania zysków. Długie zadania mogą być szkodliwe.

Prześlij opinię

  • Prześlij opinię na temat specyfikacji w repozytorium is-input-pending.
  • Napisz na Twitterze @acomminos (jeden z autorów specyfikacji).

Podsumowanie

Cieszymy się, że isInputPending() jest już dostępne i że deweloperzy mogą z niego korzystać już dziś. To pierwszy raz, gdy Facebook stworzył nowy internetowy interfejs API i przekształcił je od inkubacji pomysłów do propozycji standardów, a następnie do faktycznej dostawy w przeglądarce. Chcielibyśmy podziękować wszystkim, którzy pomogli nam dotrzeć do tego miejsca. Szczególne podziękowania kierujemy do wszystkich pracowników Chrome, którzy pomogli nam w rozwinięciu tego pomysłu i wprowadzeniu go na rynek.

Zdjęcie powitalne autorstwa Will H McMahan na Unsplash.