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()w przeglądarkach opartych na Chromium od wersji 87. Żaden inny przeglądarka nie sygnalizował zamiaru udostępnienia interfejsu API.

Tło

Większość zadań w dzisiejszym ekosystemie JS jest wykonywana w ramach jednego wątku: wątku głównego. Zapewnia to deweloperom niezawodny model wykonywania, ale wygodę użytkowników (zwłaszcza szybkość działania) może znacznie obniżyć, jeśli skrypt będzie wykonywany przez długi czas. Jeśli podczas wywoływania zdarzenia wejściowego strona wykonuje dużo operacji, na przykład przetwarzanie obrazu, nie będzie ona obsługiwać zdarzenia wejściowego kliknięcia, dopóki nie zakończy się wykonywanie 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 jest coś, co musi przekazać stronie. Następnie przeglądarka może wrócić do uruchamiania bloków kodu JavaScript w miarę ich dodawania. Może to pomóc, ale może też spowodować 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 rzadziej się poddajemy, przeglądarka dłużej reaguje na zdarzenia użytkownika, co powoduje frustrację użytkowników. 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 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: harmonogramizer

Załóżmy, że wczytywanie strony wymaga od Ciebie wykonania wielu czynności blokujących wyświetlanie, np. wygenerowania znaczników z komponentów, wyodrębnienia liczb pierwszych lub narysowania fajnego ładującego się koła. 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żemy mieć dodatkowe QUANTUM milisekund opóźnienia zdarzeń.

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, a jednocześnie zapewnić, ż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 zdarzenia „ciągłe”. Obejmują one mousemove, pointermove i inne. Jeśli interesuje Cię też plonowanie tych roślin, 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, korzystając z podobnej logiki. Mamy nadzieję, że dzięki temu deweloperzy korzystający z tych frameworków będą mogli korzystać z funkcji isInputPending() w tylu bez konieczności znacznego przekształcania kodu.

Ustąpienie nie zawsze jest złe

Warto pamiętać, że w niektórych przypadkach mniejsza wydajność nie jest najlepszym rozwiązaniem. Przekazanie kontroli przeglądarce może mieć wiele przyczyn, nie tylko przetwarzanie zdarzeń wprowadzania danych, np. renderowanie i wykonywanie innych skryptów na stronie.

Czasami przeglądarka nie może prawidłowo przypisać oczekujących zdarzeń wejścia. W szczególności ustawienie złożonych klipów i masek w przypadku ramek iframe z różnych źródeł może spowodować raportowanie fałszywych wyników negatywnych (tzn. isInputPending() może nieoczekiwanie zwrócić wartość false podczas kierowania na te ramki). Jeśli Twoja witryna wymaga interakcji z 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. Podczas wykonywania pracy w tle za pomocą interfejsu API Widoczność strony możesz częściej zwalniać, opóźniać lub oddawać procesor.

Zachęcamy do ostrożnego korzystania z funkcji isInputPending(). Jeśli nie ma pracy, która blokuje użytkownika, bądź uprzejmy dla innych uczestników pętli zdarzeń i częściej oddawaj. Długie zadania mogą być szkodliwe.

Prześlij opinię

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

Podsumowanie

Cieszymy się, że isInputPending() jest już dostępne i że deweloperzy mogą z niego 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, i szczególnie wszystkim pracownikom 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.