Lepsze planowanie JS za pomocą isInputPending()

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

Nate Schloss
Nate Schloss
Andrew Comminos
Andrew Comminos

Szybkie ładowanie jest trudne. W witrynach, które renderują treści za pomocą kodu JavaScript, konieczne jest wykorzystanie kompromisu między wydajnością wczytywania a responsywnością danych wejściowych: mogą wykonywać wszystkie zadania potrzebne do wyświetlania naraz (większa wydajność wczytywania, gorsza responsywność danych wejściowych) lub podzielić zadania na mniejsze zadania, aby zachować czas na dane wejściowe i wyrenderowanie (gorsza wydajność ładowania, większa responsywność danych wejściowych).

Aby wyeliminować ten kompromis, Facebook zaproponował i zaimplementował w Chromium interfejs API isInputPending(). Dzięki niemu czas reagowania był wygodniejszy, ale nie przynosił zysku. Biorąc pod uwagę opinie o testach origin, wprowadziliśmy szereg aktualizacji API i z przyjemnością informujemy, że jest on teraz domyślnie wysyłany w Chromium 87.

Zgodność z przeglądarką

Obsługa przeglądarek

  • 87
  • 87
  • x
  • x

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

Wprowadzenie

Większość pracy w dzisiejszym ekosystemie JavaScriptu odbywa się w jednym wątku, czyli w wątku głównym. Zapewnia to deweloperom solidny model wykonywania, ale może ono bardzo negatywnie wpływać na wrażenia użytkownika (zwłaszcza responsywność), jeśli skrypt będzie wykonywany przez dłuższy czas. Jeśli np. strona wykonuje dużo pracy w czasie wywoływania zdarzenia wejściowego, nie obsługuje tego zdarzenia do czasu zakończenia tego procesu.

Obecnie sprawdzoną metodą jest rozwiązanie tego problemu przez podzielenie kodu JavaScript na mniejsze bloki. Podczas ładowania strony może uruchomić się fragment kodu JavaScript, a następnie zwrócić i przekazać kontrolę przeglądarce. Przeglądarka może wtedy sprawdzić kolejkę zdarzeń wejściowych i zobaczyć, czy jest coś, o czym musi powiadomić stronę. Następnie może wznowić uruchamianie dodawanych bloków JavaScript. Pomaga to, ale może też powodować inne problemy.

Za każdym razem, gdy strona przekazuje przeglądarce kontrolę, musi minąć trochę czasu, zanim przeglądarka sprawdzi kolejkę zdarzeń wejściowych, przetworzy zdarzenia i pobierze kolejny blok JavaScript. Podczas gdy przeglądarka szybciej reaguje na zdarzenia, wydłuża się ogólny czas wczytywania strony. Zbyt często strona wczytuje się zbyt wolno. Jeśli adresy URL są udostępniane rzadziej, przeglądarka potrzebuje więcej czasu na reagowanie na zdarzenia użytkownika, a użytkownicy się frustrują. Niefajny.

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

Chcieliśmy dowiedzieć się, jak wyglądałaby sytuacja, gdybyśmy wymyślili nową metodę ładowania stron, która wyeliminowałaby to frustrujące kompromis. Skontaktowaliśmy się w tej sprawie ze znajomymi z Chrome i przedstawiliśmy propozycję dotyczącą witryny isInputPending(). Interfejs isInputPending() API jako pierwszy stosuje koncepcję przerw w dostępie do danych wejściowych użytkownika w internecie. Dzięki niemu JavaScript może sprawdzać dane wejściowe bez zwracania się do przeglądarki.

Diagram pokazujący, że metoda isInputPending() jest dostępna w kodzie JS w celu sprawdzenia, czy istnieją oczekujące dane wejściowe użytkownika, bez konieczności całkowitego przekazywania polecenia z powrotem do przeglądarki.

Ze względu na zainteresowanie tym interfejsem API we współpracy z zespołem Chrome wdrożyliśmy i wprowadziliśmy tę funkcję w tej przeglądarce. Z pomocą inżynierów Chrome otrzymaliśmy poprawki i sprawdziliśmy je w ramach okresu próbnego (umożliwia to Chrome testowanie zmian i uzyskiwanie opinii od deweloperów przed pełnym udostępnieniem interfejsu API).

Wzięliśmy pod uwagę opinie dotyczące testowania origin oraz innych członków grupy W3C Web Performance Reporting Group, a następnie wprowadziliśmy zmiany w interfejsie API.

Przykład: algorytm szeregowania zysków

Załóżmy, że przed wczytaniem strony musisz wykonać różne czynności blokujące wyświetlanie, na przykład wygenerować znaczniki z komponentów, wyodrębnić liczby stałe lub po prostu narysować fajny wskaźnik postępu ładowania. Każdy z nich jest podzielony na odrębny element roboczy. Korzystając z wzorca algorytmu szeregowania, narysujmy, jak można 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 później funkcję processWorkQueue() w nowym makrozadaniu za pomocą setTimeout(), umożliwiamy przeglądarce zachowywanie reakcji na dane wejściowe (może ona uruchamiać moduły obsługi zdarzeń przed wznowieniem pracy), a jednocześnie działać bez zakłóceń. W związku z innymi zadaniami, które wymagają kontroli nad pętlą zdarzeń, możemy zbyt długo rozłożyć harmonogram na dłuższą metę lub zwiększyć opóźnienie zdarzeń o nawet QUANTUM milisekund.

Dobrze, ale czy możemy robić więcej? Bez dwóch zdań!

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 wywołaniu funkcji navigator.scheduling.isInputPending() będziemy mogli szybciej reagować na podawane dane, a jednocześnie mieć pewność, że działanie blokowania wyświetlania będzie przebiegać bez zakłóceń. Jeśli do czasu zakończenia pracy nie jesteśmy zainteresowani czymś innym niż wprowadzaniem (np. malowaniem), możemy również ręcznie wydłużyć QUANTUM.

Domyślnie z isInputPending() nie są zwracane zdarzenia „ciągłe”. Obejmuje to między innymi mousemove, pointermove i inne. Jeśli chcesz korzystać również z tych gier, nie ma problemu. Gdy otrzymamy obiekt isInputPending() z wartością includeContinuous ustawioną na true, wszystko będzie 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 dodają obsługę isInputPending() do swoich podstawowych bibliotek planowania, korzystając z podobnej logiki. Być może dzięki temu deweloperzy, którzy używają tych platform, będą mogli korzystać z funkcji isInputPending() „zza kulis” bez konieczności wprowadzania istotnych zmian.

Zdobywanie nie zawsze jest złe

Pamiętaj, że mniejsza liczba wyświetleń nie jest odpowiednim rozwiązaniem w każdym przypadku. Jest wiele powodów, dla których zwracanie kontroli do przeglądarki jest inne niż przetwarzanie zdarzeń wejściowych, np. renderowanie i wykonywanie innych skryptów na stronie.

Istnieją przypadki, w których przeglądarka nie jest w stanie prawidłowo przypisać oczekujących zdarzeń wejściowych. W szczególności ustawienie złożonych klipów i masek na potrzeby elementów iframe z innych domen może zgłaszać wyniki fałszywie negatywne (np. element isInputPending() może nieoczekiwanie zwrócić wynik „fałsz” podczas kierowania na te ramki). Jeśli witryna wymaga interakcji ze stylizowanymi ramkami podrzędnymi, upewnij się, że Twoje reklamy są wystarczająco często wyświetlane.

Zwróć też uwagę na inne strony, które korzystają z tej samej pętli zdarzeń. Na platformach takich jak Chrome na Androida wiele źródeł może współdzielić pętlę zdarzeń. isInputPending() nigdy nie zwraca wartości true, jeśli dane wejściowe są wysyłane do ramki z innych domen, dlatego strony działające w tle mogą zakłócać czas reakcji stron na pierwszym planie. Podczas wykonywania pracy w tle za pomocą interfejsu Widoczność strony możesz ograniczyć, odłożyć na później czas lub zmniejszyć częstotliwość wykonywania tych operacji.

Zalecamy korzystanie z usługi isInputPending() według rozsądku. Jeśli nie ma zadań blokujących użytkowników, zachowaj uprzejmość wobec innych w pętli zdarzeń, częściej utrzymując wyniki. Długie zadania mogą być szkodliwe.

Prześlij opinię

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

Podsumowanie

Cieszymy się, że usługa isInputPending() jest już dostępna i że deweloperzy mogą zacząć z niej korzystać już dziś. To pierwszy raz, gdy Facebook stworzył nowy internetowy interfejs API i przeniósł go od inkubacji pomysłów do oferty standardów, a następnie do faktycznego przesyłania danych w przeglądarce. Dziękujemy wszystkim, którzy pomogli nam dotrzeć do tego miejsca, i podziękowania dla wszystkich, którzy pomogli nam wcielić ten pomysł w życie.

Zdjęcie produktu: Will H McMahan, w serwisie Unsplash.