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 ładowanie 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 wyeliminować konieczność kompromisu, Facebook zaproponował i wdrożył interfejs API isInputPending() w Chromium, aby poprawić czas reagowania bez generowania strat. 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()w przeglądarkach opartych na Chromium od wersji 87. Żaden inny przeglądarka nie sygnalizował zamiaru udostępnienia interfejsu API.

Tło

Większość pracy w dzisiejszym ekosystemie JS jest wykonywana w ramach jednego wątku: wątku głównego. Zapewnia to deweloperom niezawodny model wykonywania, ale jeśli skrypt jest uruchamiany przez długi czas, może to znacznie pogorszyć komfort korzystania z aplikacji (zwłaszcza szybkość działania). Jeśli na przykład podczas wywoływania zdarzenia wejściowego strona wykonuje dużo pracy, nie uwzględni zdarzenia wejściowego kliknięcia, dopóki ten proces się nie zakończy.

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. Następnie przeglądarka może wrócić do uruchamiania bloków kodu JavaScript w miarę ich dodawania. 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. Chociaż przeglądarka reaguje szybciej na zdarzenia, ogólny czas ładowania strony wydłuża się. Jeśli zbyt często się poddajemy, strona wczytuje się zbyt wolno. Jeśli generujemy rzadziej wyniki, przeglądarka potrzebuje więcej czasu na reagowanie na zdarzenia użytkowników, co powoduje 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 API isInputPending() jako pierwszy wykorzystuje koncepcję przerw w przypadku danych wejściowych użytkownika w internecie i umożliwia JavaScriptowi sprawdzanie danych wejściowych bez przekazywania ich przeglądarce.

Diagram isInputPending() pozwala JS sprawdzić, czy oczekują jakieś dane wejściowe użytkownika, bez zwracania kodu 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 wykonania wiele zadań, które blokują wyświetlanie strony, aby ją wczytać, np. generowanie znaczników z komponentów, wyodrębnianie czynników pierwszych lub po prostu rysowanie fajnego wskaźnika ładowania. Każdy z nich jest podzielony na osobne zadanie. Korzystając z wzorca harmonogramu, zarysujmy, jak moglibyśmy przetworzyć 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 zostać odroczony na długi czas przez inne zadanie, które chce przejąć kontrolę nad pętlą zdarzeń, lub może wystąpić opóźnienie zdarzeń o dodatkowe QUANTUM milisekund.

To jest w porządku, ale czy możemy zrobić coś 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 zakończenia prac nie zajmujemy się niczym innym niż wprowadzanie tekstu (np. malowaniem), możemy też znacznie zwiększyć długość QUANTUM.

Domyślnie zdarzenia „ciągłe” nie są zwracane z isInputPending(). 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 wprowadzają obsługę isInputPending() w podstawowych bibliotekach planowania, używając 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.

Ustąpienie nie zawsze jest złe

Warto pamiętać, że w niektórych przypadkach mniejsza wydajność nie jest najlepszym rozwiązaniem. Jest wiele powodów, by zwrócić do przeglądarki elementy sterujące niż przetwarzanie zdarzeń wejściowych, np. wykonanie renderowania i uruchamianie innych skryptów na stronie.

W niektórych przypadkach przeglądarka nie jest w stanie poprawnie przypisać oczekujących zdarzeń wejściowych. W szczególności ustawienie złożonych klipów i masek w przypadku ramek iframe z różnych źródeł może spowodować fałszywie negatywne wyniki (czyli isInputPending() może nieoczekiwanie zwrócić wartość false podczas kierowania na te ramki). Jeśli Twoja witryna wymaga interakcji ze stylizowanymi podramkami, upewnij się, że yielding jest wystarczająco częste.

Zwróć uwagę na inne strony, które również udostępniają pętlę zdarzeń. Na platformach takich jak Chrome na Androida dość często wiele źródeł ma wspólną pętlę 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. Podczas pracy w tle możesz ograniczyć, opóźnić lub zwiększyć zyski, używając interfejsu Page Visibility API.

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 wprowadzamy usługę isInputPending(), a deweloperzy mogą zacząć z niej korzystać już dziś. To pierwszy raz, kiedy Facebook stworzył nowy interfejs API internetowy i przeszedł z fazy pomysłu przez propozycję standardu do wdrożenia 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.