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

Moduły JavaScript w Web Workspace ułatwiają teraz przenoszenie ciężkich zadań do wątków w tle.

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ż stanowić problem, gdy musimy wykonywać najtrudniejsze zadania, takie jak przetwarzanie danych, analizowanie, obliczanie czy analiza. W miarę pojawiania się przez internet coraz bardziej złożonych aplikacji rośnie zapotrzebowanie na przetwarzanie wielowątkowe.

Na platformie internetowej głównym podstawowym elementem obsługi wątków i równoległości jest WebWorkers API. Instancje robocze stanowią lekką abstrakcję poza wątkami systemu operacyjnego, które ujawniają komunikat, który przekazuje interfejs API na potrzeby komunikacji między wątkami. Może to być bardzo przydatne podczas wykonywania kosztownych obliczeń lub pracy na dużych zbiorach danych, ponieważ umożliwia płynne działanie wątku głównego przy wykonywaniu kosztownych operacji na jednym lub kilku wątkach w tle.

Oto typowy przykład użycia instancji roboczych, 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 doskonałą obsługę przeglądarek i dobrze zoptymalizowaną optymalizację, ale oznacza to również, że korzystają z długich modułów JavaScript. Ponieważ w projektowaniu instancji roboczych nie było systemu modułów, interfejs API służący do wczytywania kodu do instancji roboczej i tworzenia skryptów pozostaje podobny do synchronicznych metod wczytywania skryptów popularnych w 2009 roku.

Historia: klasyczne instancje robocze

Konstruktor instancji roboczych pobiera adres URL klasycznego skryptu zależny od adresu URL dokumentu. Natychmiast zwraca odwołanie do nowej instancji roboczej, która ujawnia interfejs przesyłania wiadomości oraz metodę terminate(), która natychmiast się zatrzymuje i niszczy instancję roboczą.

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

Funkcja importScripts() jest dostępna w instancjach roboczych do wczytywania dodatkowego kodu, ale wstrzymuje jej wykonanie, aby pobrać i ocenić każdy skrypt. Wykonuje on również skrypty w zakresie globalnym, jak klasyczny tag <script>, co oznacza, że zmienne w jednym skrypcie mogą być zastępowane 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 rozwiązania internetowe w przeszłości wywierały ogromny wpływ na architekturę aplikacji. Programiści musieli opracować sprytne narzędzia i rozwiązania, które pozwoliłyby na wykorzystanie pracowników sieciowych bez rezygnowania z nowoczesnych rozwiązań programistycznych. Na przykład pakiety tworzące pakiet, takie jak webpack, umieszczają w wygenerowanym kodzie małą implementację modułu wczytującego kod, który używa do wczytywania kodu importScripts(), ale umieszcza moduły w funkcjach, aby uniknąć kolizji zmiennych i symulować importy i eksporty zależności.

Wpisz instancje robocze modułu

W Chrome 80 dostępny jest nowy tryb dla pracowników WWW, który zapewnia ergonomię i ulepszenia w zakresie wydajności modułów JavaScript. Konstruktor Worker akceptuje teraz nową opcję {type:"module"}, która zmienia wczytywanie i wykonanie skryptu na wartość <script type="module">.

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

Moduły robocze są standardowymi modułami JavaScript, więc mogą używać instrukcji importowania i eksportowania. Tak jak w przypadku wszystkich modułów JavaScriptu zależności są wykonywane tylko raz w danym kontekście (wątek główny, instancja robocza itd.), a wszystkie przyszłe importy będą odnosić się do już wykonywanej instancji modułu. Wczytywanie i wykonywanie modułów JavaScript również jest optymalizowane przez przeglądarki. Zależności modułu mogą być wczytywane przed jego uruchomieniem, co umożliwia równoległe ładowanie całych drzew modułów. Ładowanie modułu również buforuje przeanalizowany kod, co oznacza, że moduły używane w wątku głównym i w instancji roboczej trzeba przeanalizować tylko raz.

Przejście do modułów JavaScript umożliwia też użycie importu dynamicznego do leniwego ładowania kodu bez blokowania wykonywania instancji roboczej. Import dynamiczny jest znacznie bardziej zrozumiały niż używanie interfejsu importScripts() do wczytywania zależności, ponieważ eksporty zaimportowanego modułu są zwracane zamiast użycia 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() nie jest dostępna w instancjach roboczych modułów. Przełączenie instancji roboczych na moduły JavaScript oznacza, że cały kod jest ładowany w trybie ścisłym. Kolejną istotną zmianą jest to, że wartość this w zakresie najwyższego poziomu modułu JavaScript wynosi undefined, podczas gdy w klasycznych instancjach roboczych wartością jest globalny zakres instancji roboczej. Na szczęście zawsze istniał element globalny self, który odwołuje się do zakresu globalnego. Jest dostępny we wszystkich typach instancji roboczych, w tym w skryptach service worker, a także w modelu DOM.

Wstępnie wczytuj instancje robocze za pomocą: modulepreload

Istotnym wzrostem wydajności, który zapewnia instancje robocze modułów, jest możliwość wstępnego wczytywania instancji roboczych i ich zależności. Instancje robocze modułów są wczytywane i uruchamiane jako standardowe moduły JavaScript, co oznacza, że można je wstępnie wczytywać, a nawet przeanalizować za pomocą 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ć używane zarówno przez instancje robocze wątku głównego, jak i moduły. Jest to przydatne w przypadku modułów importowanych w obu kontekstach lub w przypadkach, gdy nie można z wyprzedzeniem sprawdzić, czy dany moduł będzie używany w wątku głównym, czy w instancji roboczej.

Wcześniej opcje dostępne do wstępnego wczytywania skryptów Web Worker były ograniczone i niepotrzebnie niezawodne. Klasyczne instancje robocze miały własny typ zasobu „robocza” do wstępnego wczytywania, ale w żadnej przeglądarce nie zaimplementowano elementu <link rel="preload" as="worker">. W związku z tym podstawową metodą dostępną do wstępnego wczytywania instancji internetowych było skorzystanie z metody <link rel="prefetch">, która opierała się całkowicie na pamięci podręcznej HTTP. Użycie ich w połączeniu z prawidłowymi nagłówkami pamięci podręcznej pozwoliło uniknąć sytuacji, w której instancja instancji roboczej musi czekać na pobranie skryptu instancji roboczej. W przeciwieństwie do modelu modulepreload ta metoda nie obsługiwała jednak wstępnego wczytywania zależności ani wstępnej analizy.

A co z współdzielonymi pracownikami?

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

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

Przed obsługą modułów JavaScript konstruktor SharedWorker() wymagał jedynie adresu URL i opcjonalnego argumentu name. Klasyczne współdzielone instancje robocze nadal będą działać, jednak tworzenie współdzielonych instancji roboczych wymaga użycia nowego argumentu options. Dostępne opcje są takie same jak w przypadku dedykowanej instancji roboczej, w tym opcja name, która zastępuje poprzedni argument name.

A co z skryptem service worker?

Specyfikacja skryptu service worker została już zaktualizowana, aby obsługiwać moduł JavaScript jako punktu wejścia i używać tej samej opcji {type:"module"} co instancje robocze modułu, jednak ta zmiana nie została jeszcze wprowadzona w przeglądarkach. Potem będzie można utworzyć instancję skryptu service worker za pomocą modułu JavaScript, korzystając z tego kodu:

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

Po zaktualizowaniu specyfikacji przeglądarki zaczynają wdrażać nowy sposób działania. Jest to czasochłonne, ponieważ dodanie modułów JavaScript do skryptu service worker wiąże się z pewnymi dodatkowymi komplikacjami. Rejestracja skryptu service worker musi porównać zaimportowane skrypty z ich poprzednimi wersjami w pamięci podręcznej, aby ustalić, czy ma aktywować aktualizację. Jest to konieczne w przypadku modułów JavaScript używanych przez mechanizmy Service Worker. W pewnych przypadkach mechanizmy Service Worker muszą też mieć możliwość pomijania pamięci podręcznej skryptów w przypadku sprawdzania dostępności aktualizacji.

Dodatkowe materiały i dalsze materiały