Użyj robotów internetowych do uruchamiania JavaScriptu poza głównym wątkiem przeglądarki

Architektura niezwiązanych z wątkiem może znacznie zwiększyć niezawodność i wygodę użytkowników aplikacji.

W ciągu ostatnich 20 lat sieć gwałtownie się rozwinęła – od dokumentów statycznych z kilkoma stylami i obrazami do złożonych, dynamicznych aplikacji. Jedna rzecz pozostaje jednak bez zmian: mamy tylko jeden wątek na kartę przeglądarki (z kilkoma wyjątkami) do renderowania witryn i uruchamiania kodu JavaScript.

W rezultacie wątek główny jest nadmiernie przeciążony. W miarę jak aplikacje internetowe stają się coraz bardziej złożone, wątek główny staje się poważnym wąskim gardłem wydajności. Co gorsza, czas potrzebny na uruchomienie kodu w wątku głównym dla danego użytkownika jest prawie całkowicie nieprzewidywalny, ponieważ możliwości urządzenia mają ogromny wpływ na wydajność. Ta nieprzewidywalność będzie rosła dopiero wtedy, gdy użytkownicy będą korzystali z internetu za pomocą coraz bardziej zróżnicowanych urządzeń – od telefonów z bardzo ograniczonymi funkcjami po flagowe urządzenia o dużej mocy obliczeniowej i wysokiej częstotliwości odświeżania.

Jeśli zależy nam, aby zaawansowane aplikacje internetowe niezawodnie spełniały wytyczne dotyczące wydajności, takie jak podstawowe wskaźniki internetowe (oparte na danych empirycznych dotyczących ludzkiego postrzegania i psychologii), musimy mieć możliwość uruchomienia naszego kodu poza głównym wątkiem (OMT).

Dlaczego Web Works

JavaScript to domyślnie język jednowątkowy, który uruchamia zadania w wątku głównym. Skrypty internetowe zapewniają jednak możliwość wyjścia z wątku głównego, umożliwiając programistom tworzenie osobnych wątków do pracy z wątkiem głównym. Zakres narzędzi internetowych jest ograniczony i nie zapewnia bezpośredniego dostępu do modelu DOM, ale może być bardzo przydatny, jeśli wymagane jest wykonanie dużej ilości pracy, które w przeciwnym razie przeciążyłoby główny wątek.

W przypadku podstawowych wskaźników internetowych korzystne może być przeprowadzenie pracy poza głównym wątkiem. W szczególności przeniesienie zadań z wątku głównego do instancji roboczych może ograniczyć rywalizację w wątku głównym, co może poprawić wskaźnik responsywności strony od interakcji do kolejnego wyrenderowania (INP). Gdy wątek główny ma mniej pracy, może szybciej reagować na interakcje użytkowników.

Mniej pracy w głównym wątku – zwłaszcza podczas uruchamiania – może też wiązać się z potencjalną korzyścią w przypadku największego wyrenderowania treści (LCP), ponieważ skracają długie zadania. Renderowanie elementu LCP wymaga czasu głównego wątku – zarówno w przypadku renderowania tekstu lub obrazów, które są częstymi i częstymi elementami LCP. Zmniejszając ogólną pracę wątku głównego, możesz ograniczyć ryzyko zablokowania elementu LCP przez kosztowną pracę, którą mogłaby wykonać robot internetowy.

Tworzenie wątków na potrzeby instancji roboczych

Inne platformy zwykle obsługują pracę równoległą, umożliwiając przypisanie do wątku funkcji, która działa równolegle z resztą programu. Możesz uzyskać dostęp do tych samych zmiennych w obu wątkach, a dostęp do tych udostępnionych zasobów można zsynchronizować z muteksami i semhorami, aby zapobiec warunkom wyścigu.

W języku JavaScript możemy uzyskać podobną funkcjonalność w środowiskach roboczych, które działają od 2007 roku i są obsługiwane we wszystkich popularnych przeglądarkach od 2012 roku. Instancje robocze działają równolegle z wątkiem głównym, ale w przeciwieństwie do podziału systemu operacyjnego na wątki nie mogą udostępniać zmiennych.

Aby utworzyć instancję roboczą, przekaż plik do konstruktora instancji roboczej, który uruchamia ten plik w osobnym wątku:

const worker = new Worker("./worker.js");

Komunikuj się z pracownikiem sieci, wysyłając wiadomości za pomocą interfejsu API postMessage. Przekaż wartość wiadomości jako parametr w wywołaniu postMessage, a potem dodaj do instancji roboczej detektor zdarzeń wiadomości:

main.js

const worker = new Worker('./worker.js');
worker.postMessage([40, 2]);

worker.js

addEventListener('message', event => {
  const [a, b] = event.data;

  // Do stuff with the message
  // ...
});

Aby wysłać wiadomość z powrotem do wątku głównego, użyj tego samego interfejsu API postMessage w narzędziu Web Worker i skonfiguruj detektor zdarzeń w wątku głównym:

main.js

const worker = new Worker('./worker.js');

worker.postMessage([40, 2]);
worker.addEventListener('message', event => {
  console.log(event.data);
});

worker.js

addEventListener('message', event => {
  const [a, b] = event.data;

  // Do stuff with the message
  postMessage(a + b);
});

Przyznaję, że takie podejście jest nieco ograniczone. W przeszłości narzędzia internetowe były głównie wykorzystywane do przenoszenia pojedynczych zadań z głównego wątku. Obsługa wielu operacji w ramach jednego mechanizmu sieciowego staje się nieporęczna: trzeba zakodować nie tylko parametry, ale również operację w wiadomości i prowadzić księgowość, aby dopasować odpowiedzi na żądania. Z tej złożoności wynika prawdopodobnie, że metody pracy w internecie nie są szersze.

Gdybyśmy jednak mogli usunąć część trudności w komunikacji między wątkiem głównym a procesorami internetowymi, ten model świetnie się sprawdziłby w wielu zastosowaniach. Na szczęście istnieje biblioteka, która spełnia to zadanie.

Comlink to biblioteka, której celem jest umożliwienie korzystania z narzędzi internetowych bez konieczności zagłębiania się w szczegóły postMessage. Comlink umożliwia współdzielenie zmiennych między instancjami roboczymi internetowymi a wątkiem głównym, prawie tak jak w przypadku innych języków programowania, które obsługują wątki.

Aby skonfigurować usługę Comlink, zaimportuj ją do instancji roboczej i zdefiniuj zestaw funkcji, które będą udostępniane w wątku głównym. Następnie zaimportuj Comlink w wątku głównym, zapakuj instancję roboczą i uzyskasz dostęp do ujawnionych funkcji:

worker.js

import {expose} from 'comlink';

const api = {
  someMethod() {
    // ...
  }
}

expose(api);

main.js

import {wrap} from 'comlink';

const worker = new Worker('./worker.js');
const api = wrap(worker);

Zmienna api w wątku głównym działa tak samo jak zmienna w narzędziu internetowym, z tą różnicą, że każda funkcja zwraca obietnicę wartości, a nie jej samej.

Jaki kod należy przenieść do narzędzia Web Worker?

Skrypty internetowe nie mają dostępu do DOM ani wielu interfejsów API, takich jak WebUSB, WebRTC czy Web Audio, więc nie można umieścić w nich fragmentów aplikacji, które wymagają takiego dostępu. Mimo to każdy mały fragment kodu przeniesiony do pracownika kupuje więcej miejsca w wątku głównym na potrzeby treści, które muszą tam być, jak np. aktualizacja interfejsu użytkownika.

Jednym z problemów programistów internetowych jest to, że większość aplikacji internetowych do administrowania całą zawartością aplikacji bazuje na platformie UI, takiej jak Vue czy React. Wszystkie elementy są elementem platformy i są z założenia powiązane z DOM. Mogłoby to utrudnić migrację na architekturę OMT.

Jeśli jednak przejdziemy do modelu, w którym potencjalne problemy z interfejsem są oddzielone od innych kwestii, takich jak zarządzanie państwem, pracownicy internetowi mogą być całkiem przydatne nawet w przypadku aplikacji opartych na platformie. Takie podejście stosujemy dzięki PROXX.

PROXX: studium przypadku OMT

Zespół Google Chrome opracował PROXX jako kopię sapera, która spełnia wymagania progresywnej aplikacji internetowej, w tym możliwość pracy w trybie offline i zapewniania użytkownikom wysokiej jakości środowiska. Niestety wczesne wersje gry nie radziły sobie dobrze na urządzeniach z ograniczonym dostępem, takich jak telefony z podstawową przeglądarką, przez co członkowie zespołu zdali sobie sprawę, że główny wątek jest wąskim gardłem.

Zespół zdecydował się wykorzystać instancje internetowe, aby oddzielić obrazowy stan gry od jej logiki:

  • Wątek główny obsługuje renderowanie animacji i przejść.
  • Robot internetowy obsługuje logikę gry, która jest czysto obliczeniowa.
.

OMT miała interesujący wpływ na wydajność telefonów z podstawową przeglądarką PROXX. W wersji innej niż OMT interfejs użytkownika zawiesza się na 6 sekund po interakcji z nim. Nie ma żadnych informacji zwrotnych, a użytkownik musi czekać pełne 6 sekund, zanim będzie mógł zrobić coś innego.

Czas odpowiedzi interfejsu w wersji innej niż OMT PROXX.

Jednak w wersji OMT aktualizacja interfejsu gry trwa dwanaście sekund. Choć wygląda to na spadek skuteczności, w rzeczywistości powoduje to wzrost opinii użytkowników. Spowolnienie występuje, ponieważ aplikacja wysyła więcej klatek niż wersja inna niż OMT, co w ogóle nie wysyła żadnych klatek. Dzięki temu użytkownik wie, że coś się dzieje, i może kontynuować rozgrywkę w miarę aktualizowania interfejsu, dzięki czemu gra jest znacznie lepsza.

Czas odpowiedzi interfejsu w wersji OMT PROXX.

Jest to świadomy kompromis: użytkownikom urządzeń z ograniczonym dostępem chcemy zapewnić wrażenia lepsze, nie nakładając kar na użytkowników zaawansowanych urządzeń.

Konsekwencje architektury OMT

Jak widać na przykładzie PROXX, OMT sprawia, że aplikacja działa niezawodnie na większej liczbie urządzeń, ale nie przyspiesza jej działania:

  • Po prostu przenosisz pracę z głównego wątku, a nie ograniczasz go.
  • Dodatkowy narzut związany z komunikacją między pracownikiem WWW a wątek główny może czasami nieco spowalniać działanie.

Rozważ wady i zalety

Wątek główny może dowolnie przetwarzać interakcje użytkowników, takie jak przewijanie, gdy działa JavaScript, więc zmniejsza liczbę utraconych klatek, mimo że całkowity czas oczekiwania może być nieco dłuższy. Zalecamy skrócenie czasu oczekiwania przez użytkownika, by usunąć klatkę, ponieważ w przypadku utraconych klatek margines błędu jest mniejszy: usunięcie klatki odbywa się w milisekundach, a użytkownicy mają czas oczekiwania na setki milisekund.

Z powodu nieprzewidywalności wydajności na różnych urządzeniach celem architektury OMT jest tak naprawdę zmniejszenie ryzyka, czyli zwiększenie wytrzymałości aplikacji w bardzo zmiennych warunkach działania, a nie na zwiększeniu wydajności wynikającej z adaptacji równoległej. Zwiększenie odporności i ulepszenie UX jest warte więcej niż jakikolwiek drobny kompromis w szybkości.

Uwaga na temat narzędzi

Internetowe procesy robocze nie są jeszcze popularne, dlatego większość narzędzi modułów (takich jak webpack i Rollup) nie obsługuje ich od razu. (ale pakiet!) Na szczęście są dostępne wtyczki, które współpracują z Webpack i Rollup:

Podsumowanie

Aby nasze aplikacje były jak najbardziej niezawodne i dostępne, zwłaszcza w coraz bardziej globalnym rynku, musimy obsługiwać urządzenia z ograniczeniami. To właśnie w ten sposób większość użytkowników korzysta z internetu na całym świecie. OMT to obiecujący sposób na zwiększenie wydajności na takich urządzeniach bez negatywnego wpływu na użytkowników zaawansowanych urządzeń.

OMT ma też dodatkowe korzyści:

Pracownicy internetowi nie muszą się bać. Narzędzia takie jak Comlink zmniejszą obciążenie pracowników i sprawiają, że są one dobrym wyborem dla szerokiej gamy aplikacji internetowych.

Baner powitalny z filmu Unsplash, autorstwa James Peacock.