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 pozostała jednak w dużej mierze niezmieniona: mamy tylko 1 wątek na kartę przeglądarki (z pewnymi wyjątkami), który odpowiada za renderowanie witryn i wykonywanie kodu JavaScript.

W rezultacie wątek główny stał się bardzo obciąż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 wykonywania kodu w głównym wątku przez danego użytkownika jest prawie całkowicie nieprzewidywalny, ponieważ możliwości urządzenia mają ogromny wpływ na wydajność. Ta nieprzewidywalność będzie się tylko pogłębiać, ponieważ użytkownicy będą korzystać z internetu na coraz bardziej zróżnicowanych urządzeniach, od bardzo ograniczonych telefonów z podstawową przeglądarką po flagowe urządzenia o wysokiej mocy i częstotliwości odświeżania.

Jeśli chcemy, aby zaawansowane aplikacje internetowe niezawodnie spełniały wytyczne dotyczące wydajności, takie jak podstawowe wskaźniki internetowe, które opierają się na danych empirycznych dotyczących ludzkiego postrzegania i psychologii, potrzebujemy sposobów na wykonywanie kodu poza wątkiem głównym (OMT).

Dlaczego web workery?

JavaScript jest domyślnie językiem jednowątkowym, który wykonuje zadaniawą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 obsługi pracy z wątkiem głównym. Chociaż zakres działania web workers jest ograniczony i nie zapewnia bezpośredniego dostępu do DOM, mogą one być bardzo przydatne, jeśli trzeba wykonać dużą ilość pracy, która w przeciwnym razie przytłoczyłaby główny wątek.

W przypadku podstawowych wskaźników internetowych korzystne może być przeprowadzenie pracy poza głównym wątkiem. Przeniesienie pracy z głównego wątku do instancji roboczych może zmniejszyć współzawodnictwo o główny wątek, co może poprawić wartość interakcji do kolejnego wyrenderowania (INP) strony. Gdy wątek główny ma mniej pracy do wykonania, może szybciej reagować na interakcje użytkownika.

Mniej pracy wątku głównego, zwłaszcza podczas uruchamiania, może też przynieść korzyści w zakresie największego wyrenderowania treści (LCP), ponieważ skraca czas wykonywania długich zadań. 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, masz pewność, że element LCP na stronie nie zostanie zablokowany przez kosztowną pracę, jaką mogłaby wykonać robot internetowy.

Przetwarzanie wątków za pomocą procesów wątekowych w przeglądarce

Inne platformy zwykle obsługują pracę równoległą, umożliwiając przypisanie wątkowi 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 JavaScript możemy uzyskać mniej więcej podobną funkcjonalność dzięki procesom w tle, które są dostępne od 2007 r. i obsługiwane we wszystkich głównych przeglądarkach od 2012 r. Instancje robocze działają równolegle z wątkiem głównym, ale w przeciwieństwie do wątków systemu operacyjnego nie mogą udostępniać zmiennych.

Aby utworzyć proces roboczy w sieci, prześlij plik do konstruktora procesu roboczego, który rozpocznie jego uruchamianie w osobnym wątku:

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

Komunikuj się z pracownikiem obsługi klienta wysyłając wiadomości za pomocą interfejsu postMessage API. 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 procesie web worker i skonfiguruj odbiornik 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);
});

Trzeba jednak przyznać, że to podejście ma pewne ograniczenia. Wcześniej web workery były używane głównie do przenoszenia pojedynczego zadania o wysokich wymaganiach 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ą wdrażane szerzej.

Gdybyśmy jednak mogli usunąć część trudności 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 właśnie to robi.

Comlink to biblioteka, która umożliwia korzystanie z elementów web worker bez konieczności zastanawiania się nad szczegółami 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 web workera?

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 tych elementów aplikacji, które wymagają takiego dostępu. Mimo to każdy mały fragment kodu przeniesiony do wątku roboczego daje więcej miejsca na głównym wątku na rzeczy, które musi się tam znajdować, np. na aktualizowanie interfejsu użytkownika.

Jednym z problemów dla deweloperów stron internetowych jest to, że większość aplikacji internetowych korzysta z ramy UI, takiej jak Vue czy React, aby zorganizować wszystko w aplikacji. Wszystko jest komponentem tej ramy i jest nierozerwalnie związane z DOM. To utrudnia migrację do architektury 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. Właśnie takie podejście zastosowaliśmy w przypadku PROXX.

PROXX: studium przypadku dotyczące OMT

Zespół Google Chrome opracował PROXX jako klona gry Minesweeper, która spełnia wymagania dotyczące progresywnych aplikacji internetowych, w tym działa offline i zapewnia atrakcyjne wrażenia użytkownika. Niestety wczesne wersje gry działały słabo na urządzeniach z ograniczonymi możliwościami, takich jak telefony komórkowe. Zespół zdał sobie sprawę, że główna nić jest wąskim gardłem.

Zespół zdecydował się użyć procesów w tle, aby oddzielić stan wizualny gry od jej logiki:

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

OMT miał ciekawy wpływ na wydajność telefonu z podstawową przeglądarką w przypadku PROXX. W wersji innej niż OMT interfejs użytkownika zawiesza się na 6 sekund po interakcji z nim. Nie ma żadnej informacji zwrotnej, a użytkownik musi odczekać 6 sekund, zanim będzie mógł wykonać inną czynność.

Czas reakcji interfejsu w wersji bez OMT PROXX.

W wersji OMT gra potrzebuje 12 sekund na aktualizację interfejsu. Może się to wydawać stratą na skuteczności, ale w rzeczywistości prowadzi do zwiększenia liczby opinii użytkowników. Spowolnienie występuje, ponieważ aplikacja wysyła więcej klatek niż wersja bez OMT, która nie wysyła żadnych klatek. Użytkownik wie więc, że dzieje się coś ważnego, i może kontynuować grę, gdy interfejs się aktualizuje, co znacznie poprawia wrażenia z gry.

Czas odpowiedzi interfejsu użytkownika 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:

  • Przenosisz tylko pracę z głównego wątku, a nie zmniejszasz jej.
  • Dodatkowe obciążenie komunikacyjne między wątkiem przeglądarki a wątkiem głównym może czasami nieznacznie spowolnić 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. Lepiej jest pozwolić użytkownikowi na chwilę poczekać, niż zrezygnować z ramki, ponieważ margines błędu jest mniejszy w przypadku pominięcia ramki: pominięcie ramki następuje w milisekundach, a masz setki milisekund, zanim użytkownik zauważy czas oczekiwania.

Ze względu na nieprzewidywalność wydajności na różnych urządzeniach celem architektury OMT jest zmniejszanie ryzyka, czyli zwiększanie odporności aplikacji na bardzo zmienne warunki działania, a nie zwiększanie wydajności dzięki równoległemu wykonywaniu. Zwiększenie odporności i ulepszenie UX jest warte więcej niż jakikolwiek drobny kompromis w szybkości.

Uwaga na temat narzędzi

Wątek web worker nie jest jeszcze powszechnie stosowany, dlatego większość narzędzi do tworzenia modułów, takich jak webpack i Rollup, nie obsługuje go domyślnie. (Parcel to obsługuje!). Na szczęście są dostępne wtyczki, które współpracują z Webpack i Rollup:

Podsumowanie

Aby zapewnić jak największą niezawodność i dostępność naszych aplikacji, zwłaszcza na coraz bardziej globalnym rynku, musimy obsługiwać urządzenia o ograniczonych możliwościach – to na nich korzysta większość użytkowników na całym świecie. OMT to obiecująca metoda zwiększania wydajności na takich urządzeniach bez negatywnego wpływu na użytkowników urządzeń wysokiej klasy.

OMT ma też inne zalety:

Pracownicy internetowi nie muszą się bać. Narzędzia takie jak Comlink odciążają pracowników i czynią ich bardziej atrakcyjnymi w przypadku wielu aplikacji internetowych.