Podział sieci na wątki za pomocą instancji roboczych modułów

Przeniesienie ciężkiej pracy do wątków w tle jest teraz łatwiejsze dzięki modułom JavaScript w modułach internetowych.

JavaScript jest jednowątkowy, co oznacza, że może wykonywać tylko jedną operację naraz. Jest to intuicyjne i sprawdza się w wielu przypadkach w internecie, ale może też sprawiać problemy, gdy musimy wykonywać ciężkie zadania, takie jak przetwarzanie, analizowanie, obliczanie czy analiza danych. W miarę jak coraz więcej złożonych aplikacji jest dostępnych w internecie, rośnie zapotrzebowanie na przetwarzanie wielowątkowe.

Na platformie internetowej głównym elementem podstawowym do obsługi wątków i równoległości jest Web Workers API. Instancje robocze to uproszczona abstrakcja dodana do wątków systemu operacyjnego, która udostępnia interfejs API przekazywania wiadomości na potrzeby komunikacji między wątkami. Może to być niezwykle przydatne w przypadku kosztownych obliczeń lub wykonywania operacji na dużych zbiorach danych, ponieważ umożliwia płynne działanie wątku głównego przy jednoczesnym wykonywaniu kosztownych operacji w jednym lub kilku wątkach w tle.

Oto typowy przykład użycia instancji roboczej, w którym skrypt instancji roboczej nasłuchuje wiadomości z wątku głównego i odpowiada, wysyłając własne wiadomości:

page.js:

const worker = new Worker('worker.js');
worker.addEventListener('message', e => {
  console.log(e.data);
});
worker.postMessage('hello');

worker.js:

addEventListener('message', e => {
  if (e.data === 'hello') {
    postMessage('world');
  }
});

Interfejs Web Worker API jest dostępny w większości przeglądarek od ponad 10 lat. Oznacza to, że pracownicy mają doskonałą obsługę przeglądarek i są dobrze zoptymalizowane, a jednocześnie mają długi czas starszych modułów JavaScript. Ponieważ podczas projektowania instancji roboczych nie było systemu modułów, interfejs API służący do wczytywania kodu do instancji roboczej i tworzenia skryptów pozostał podobny do metod synchronicznego wczytywania skryptów często używanych w 2009 roku.

Historia: klasyczne instancje robocze

Konstruktor instancji roboczej przyjmuje adres URL klasycznego skryptu, który jest zależny od adresu URL dokumentu. Natychmiast zwraca odwołanie do nowej instancji roboczej, co udostępnia interfejs do przesyłania wiadomości i metodę terminate(), która natychmiast zatrzymuje i niszczy instancję roboczą.

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

Funkcja importScripts() jest dostępna w ramach procesów sieciowych w celu wczytywania dodatkowego kodu, ale wstrzymuje wykonywanie instancji roboczej, aby pobrać i ocenić każdy skrypt. Wykonuje też skrypty w zakresie globalnym, podobnie jak klasyczny tag <script>, co oznacza, że zmienne w jednym skrypcie mogą zostać zastąpione przez zmienne w innym.

worker.js:

importScripts('greet.js');
// ^ could block for seconds
addEventListener('message', e => {
  postMessage(sayHello());
});

greet.js:

// global to the whole worker
function sayHello() {
  return 'world';
}

Z tego powodu pracownicy sieciowych w przeszłości mieli ogromny wpływ na architekturę aplikacji. Deweloperzy musieli opracować sprytne narzędzia i obejścia pozwalające na korzystanie z pracowników internetowych bez rezygnowania z nowoczesnych metod programowania. Na przykład programy do tworzenia pakietów (np. webpack) umieszczają w wygenerowanym kodzie małe moduły wczytujące kod, który do wczytywania kodu wykorzystuje importScripts(), ale pakują je w funkcje, aby uniknąć kolizji zmiennych i symulować importy i eksporty zależności.

Wpisz instancje robocze modułu

Moduły JavaScript są dostępne w Chrome 80 w nowym trybie, nazywanym modułami modułowymi, i który zapewnia korzyści płynące z ergonomii i wydajności. Konstruktor Worker akceptuje teraz nową opcję {type:"module"}, co zmienia wczytywanie i wykonywanie skryptu tak, aby odpowiadały ustawieniu <script type="module">.

const worker = new Worker('worker.js', {
  type: 'module'
});

Ponieważ instancje robocze modułów są standardowymi modułami JavaScript, mogą używać instrukcji importowania i eksportowania. Tak jak w przypadku wszystkich modułów JavaScript, zależności są wykonywane tylko raz w danym kontekście (wątek główny, instancja robocza itp.), a wszystkie przyszłe importy odwołują się do już uruchomionej instancji modułu. Wczytywanie i wykonywanie modułów JavaScript jest również optymalizowane przez przeglądarki. Zależności modułu można wczytać przed jego uruchomieniem, co pozwala wczytywać równolegle całe drzewa modułów. Podczas ładowania modułu również jest zapisywany w pamięci podręcznej przeanalizowany kod, co oznacza, że moduły używane w wątku głównym i w obrębie instancji roboczej wymagają analizy tylko raz.

Przejście do modułów JavaScript umożliwia też korzystanie z importu dynamicznego na potrzeby leniwego ładowania kodu bez blokowania wykonania instancji roboczej. Import dynamiczny jest znacznie bardziej jawny niż użycie metody importScripts() do wczytywania zależności, ponieważ eksporty zaimportowanego modułu są zwracane, a nie polegają na zmiennych globalnych.

worker.js:

import { sayHello } from './greet.js';
addEventListener('message', e => {
  postMessage(sayHello());
});

greet.js:

import greetings from './data.js';
export function sayHello() {
  return greetings.hello;
}

Aby zapewnić wysoką wydajność, stara metoda importScripts() jest niedostępna w instancjach roboczych modułów. Przełączenie instancji roboczych na moduły JavaScript oznacza, że cały kod będzie wczytywany w trybie rygorystycznym. Kolejną znaczącą zmianą jest to, że wartość this w zakresie najwyższego poziomu modułu JavaScript wynosi undefined, podczas gdy w klasycznych instancjach roboczych wartość to jej zakres globalny. Na szczęście zawsze istniał globalny self, który odwołuje się do zakresu globalnego. Jest dostępny we wszystkich typach instancji roboczych, w tym skryptach service worker oraz w DOM.

Wstępnie ładuj instancje robocze za pomocą funkcji modulepreload

Znaczącą poprawę wydajności, która jest dostępna w przypadku instancji roboczych modułów, jest możliwość wstępnego wczytywania instancji roboczych i ich zależności. W przypadku instancji roboczych modułów skrypty są wczytywane i wykonywane jako standardowe moduły JavaScript, co oznacza, że mogą być wstępnie wczytywane, a nawet gotowe do analizy za pomocą narzędzia modulepreload:

<!-- preloads worker.js and its dependencies: -->
<link rel="modulepreload" href="worker.js">

<script>
  addEventListener('load', () => {
    // our worker code is likely already parsed and ready to execute!
    const worker = new Worker('worker.js', { type: 'module' });
  });
</script>

Wstępnie załadowane moduły mogą być też używane przez instancje robocze wątku głównego i modułów roboczych modułu. Jest to przydatne w przypadku modułów importowanych w obu kontekstach lub w przypadkach, gdy nie można z wyprzedzeniem określić, czy dany moduł będzie używany w wątku głównym czy w instancji roboczej.

Wcześniej opcje wstępnego wczytywania skryptów instancji roboczych były ograniczone i niepotrzebnie niezawodne. Klasyczne instancje robocze miały własny typ zasobów „instancja robocza” do wstępnego wczytywania, ale żadna przeglądarka nie wdrożyła <link rel="preload" as="worker">. W rezultacie podstawową metodą dostępną do wstępnego wczytywania instancji roboczych było użycie <link rel="prefetch">, który w całości polegał na pamięci podręcznej HTTP. W połączeniu z prawidłowymi nagłówkami buforowania pozwoliło to uniknąć konieczności oczekiwania na pobranie skryptu instancji roboczej. Jednak w przeciwieństwie do funkcji modulepreload ta metoda nie obsługiwała wstępnego wczytywania zależności ani przygotowań.

A co z pracownikami współużytkowanymi?

W Chrome 83 współdzielone instancje robocze zostały zaktualizowane o obsługę modułów JavaScript. Podobnie jak w przypadku dedykowanych instancji roboczych utworzenie współdzielonej instancji roboczej z opcją {type:"module"} wczytuje teraz skrypt jako moduł, a nie klasyczny skrypt:

const worker = new SharedWorker('/worker.js', {
  type: 'module'
});

Przed obsługą modułów JavaScript konstruktor SharedWorker() oczekiwał tylko adresu URL i opcjonalnego argumentu name. Będzie to nadal działać w przypadku klasycznego współużytkowania instancji roboczych, ale do utworzenia współdzielonych instancji roboczych modułu konieczne jest użycie nowego argumentu options. Dostępne opcje są takie same jak w przypadku dedykowanej instancji roboczej. Obejmuje to opcję name, która zastępuje poprzedni argument name.

A co z skryptem service worker?

Specyfikacja skryptu service worker została już zaktualizowana, aby umożliwić akceptowanie modułu JavaScript jako punktu wejścia przy użyciu tej samej opcji {type:"module"} co instancje robocze modułu. Zmiana ta nie została jeszcze zaimplementowana w przeglądarkach. Gdy to nastąpi, będzie można utworzyć instancję skryptu service worker za pomocą modułu JavaScript za pomocą następującego kodu:

navigator.serviceWorker.register('/sw.js', {
  type: 'module'
});

Po zaktualizowaniu specyfikacji przeglądarki zaczynają implementować nowy sposób działania. To zajmuje trochę czasu, ponieważ wiążą się z tym dodatkowe komplikacje związane z dodawaniem modułów JavaScript do skryptu service worker. Rejestracja skryptu service worker musi porównywać zaimportowane skrypty z ich poprzednimi wersjami z pamięci podręcznej, aby określić, czy ma aktywować aktualizację. Trzeba to zaimplementować dla modułów JavaScript, gdy są używane na potrzeby skryptów service worker. W niektórych przypadkach mechanizmy Service Worker muszą mieć możliwość ominięcia pamięci podręcznej skryptów podczas sprawdzania dostępności aktualizacji.

Dodatkowe materiały i materiały dodatkowe