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

Przenoszenie obciążeń na wątki w tle jest teraz łatwiejsze dzięki modułom JavaScript w web workerach.

Kod JavaScriptu jest jednowątkowy, co oznacza, że może wykonywać tylko jedną operację naraz. Jest to intuicyjne i działa dobrze w wielu przypadkach w internecie, ale może stać się problematyczne, gdy trzeba wykonać skomplikowane zadania, takie jak przetwarzanie danych, parsowanie, obliczenia czy analiza. Wraz z tym, jak w sieci pojawia się coraz więcej złożonych aplikacji, rośnie zapotrzebowanie na przetwarzanie wielowątkowe.

Na platformie internetowej głównym narzędziem do obsługi wątków i paralelizowania jest interfejs WebWorkers API. Workery to lekka abstrakcja na poziomie wątków systemu operacyjnego, która udostępnia interfejs API do przesyłania komunikatów na potrzeby komunikacji między wątkami. Może to być bardzo przydatne podczas wykonywania kosztownych obliczeń lub pracy z bardzo dużymi zbiorami danych, ponieważ pozwala wątkowi głównemu działać płynnie podczas wykonywania kosztownych operacji na co najmniej jednym wątku w tle.

Oto typowy przykład użycia workera, w którym skrypt workera nasłuchuje wiadomości z głównego wątku 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 roboty są doskonale obsługiwane przez przeglądarki i dobrze zoptymalizowane, ale też, że istnieją na długo przed modułami JavaScript. Ponieważ w czasie projektowania instancji roboczych nie istniał system modułów, interfejs API do wczytywania kodu do instancji roboczej i tworzenia skryptów pozostał podobny do synchronicznych metod wczytywania skryptów powszechnych w 2009 r.

Historia: klasyczne instancje robocze

Konstruktor Worker przyjmuje adres URL klasycznego skryptu, który jest względny względem adresu URL dokumentu. Natychmiast zwraca odwołanie do nowej instancji workera, która udostępnia interfejs przesyłania wiadomości, a także metodę terminate(), która natychmiast zatrzymuje i niszczy workera.

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

W procesach web worker dostępne jest wywołanie importScripts(), które służy do wczytywania dodatkowego kodu, ale w celu pobrania i oceny każdego skryptu proces ten musi wstrzymać swoje działanie. Wykonuje też skrypty w zakresie globalnym, podobnie jak klasyczny tag <script>, co oznacza, że zmienne w jednym skrypcie mogą być zastępowane zmiennymi z innego.

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 web workery miały w przeszłości ogromny wpływ na architekturę aplikacji. Deweloperzy musieli tworzyć sprytne narzędzia i opracowane metody, aby umożliwić korzystanie z elementów web worker bez rezygnowania z nowoczesnych metod programowania. Na przykład narzędzia do tworzenia pakietów, takie jak webpack, umieszczają w generowanym kodzie małą implementację modułu ładującego, która używa funkcji importScripts()do ładowania kodu, ale otacza moduły funkcjami, aby uniknąć kolizji zmiennych i zasymulować importowanie i eksportowanie zależności.

Wpisz instancje robocze modułu

W Chrome 80 udostępniamy nowy tryb dla procesów webowych, który zapewnia korzyści związane z ergonomiką i wydajnością modułów JavaScriptu. Tryb ten nazywa się „module workers”. Konstruktor Worker obsługuje teraz nową opcję {type:"module"}, która zmienia wczytywanie i wykonywanie skryptu zgodnie z opcją <script type="module">.

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

Ponieważ moduły robocze to standardowe moduły JavaScriptu, mogą one używać instrukcji importu i eksportu. Podobnie jak w przypadku wszystkich modułów JavaScriptu, zależności są wykonywane tylko raz w danym kontekście (wątku głównego, workera itp.), a wszystkie przyszłe importy odwołują się do już wykonanej instancji modułu. Ładowanie i wykonywanie modułów JavaScript jest również optymalizowane przez przeglądarki. Zależności modułu można wczytać przed jego wykonaniem, co pozwala wczytywać całe drzewa modułów równolegle. Ładowanie modułów powoduje też umieszczenie w pamięci podręcznej przetworzonego kodu, co oznacza, że moduły używane w głównym wątku i w instancjach roboczych muszą zostać przetworzone tylko raz.

Przejście na moduły JavaScript umożliwia też korzystanie z dynamicznego importu do leniwego wczytywania kodu bez blokowania wykonania zadania. Dynamiczny import jest znacznie bardziej jednoznaczny niż używanie importScripts() do wczytywania zależności, ponieważ zwracane są eksporty zaimportowanego modułu, a nie korzysta się z 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 module Workers. Przełączenie instancji roboczych na korzystanie z modułów JavaScript oznacza, że cały kod jest wczytywany w trybie ścisłym. Kolejną ważną zmianą jest to, że wartość this w zakresie najwyższego poziomu modułu JavaScript to undefined, podczas gdy w klasycznych instancjach roboczych jest to ich zakres globalny. Na szczęście zawsze istniała zmienna self globalna, która odwoływała się do zakresu globalnego. Jest ona dostępna dla wszystkich typów workerów, w tym service workerów, a także w DOM.

Wczytaj wstępnie instancje robocze w modulepreload

Jedną z istotnych zalet modułów workerów jest możliwość wstępnego wczytania workerów i ich zależności. W przypadku modułów roboczych skrypty są wczytywane i wykonywane jako standardowe moduły JavaScriptu, co oznacza, że można je wstępnie wczytać, a nawet wstępnie parsować 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>

Załadowane wstępnie moduły mogą być używane zarówno przez główne wątki, jak i instancje robocze modułów. Jest to przydatne w przypadku modułów importowanych w obu kontekstach lub w sytuacjach, gdy nie można z wyprzedzeniem wiedzieć, czy moduł będzie używany w wątku głównym czy w procesie pomocniczym.

Wcześniej opcje dostępne do wstępnego wczytywania skryptów web workera były ograniczone i niekoniecznie niezawodne. Klasyczne zadania miały własny typ zasobu „worker” do wstępnego wczytywania, ale nie były implementowane w żadnych przeglądarkach. <link rel="preload" as="worker"> W związku z tym główną techniką dostępną do wstępnego wczytywania web workerów było użycie <link rel="prefetch">, która całkowicie polegała na pamięci podręcznej HTTP. W połączeniu z odpowiednimi nagłówkami pamięci podręcznej umożliwiało to uniknięcie oczekiwania na pobranie skryptu instancji roboczej. Jednak w przeciwieństwie do modulepreload ta technika nie obsługiwała wstępnego wczytywania zależności ani wstępnego parsowania.

A co z pracownikami współdzielnymi?

Użytkownicy współdzieleni zostali zaktualizowani o obsługę modułów JavaScriptu w Chrome 83. Podobnie jak w przypadku dedykowanych instancji roboczych, utworzenie współdzielonej instancji roboczej za pomocą opcji {type:"module"} powoduje załadowanie skryptu instancji roboczej jako modułu, a nie klasycznego skryptu:

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

Przed wprowadzeniem obsługi modułów JavaScript konstruktor SharedWorker() oczekiwał tylko argumentu URL i opcjonalnego argumentu name. Nadal będzie to działać w przypadku klasycznych współdzielonych wątków, ale tworzenie współdzielonych wątków modułu wymaga użycia nowego argumentu options. Dostępne opcje są takie same jak w przypadku dedykowanego pracownika, w tym opcja name, która zastępuje poprzedni argument name.

A co z skryptem service worker?

Specyfikacja service worker została już zaktualizowana, aby obsługiwać moduł JavaScript jako punkt wejścia, używając tej samej opcji {type:"module"} co moduły instancji roboczej. Ta zmiana nie została jeszcze zaimplementowana w przeglądarkach. Gdy to nastąpi, będzie można utworzyć instancję service workera za pomocą modułu JavaScriptu za pomocą tego kodu:

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

Po zaktualizowaniu specyfikacji przeglądarki zaczynają wdrażać nowe zachowanie. Zajmuje to trochę czasu, ponieważ przenoszenie modułów JavaScript do usługowca jest nieco bardziej skomplikowane. Rejestracja usług powiązanych musi porównywać zaimportowane skrypty z ich poprzednimi wersjami w pamięci podręcznej, aby określić, czy należy wywołać aktualizację. Należy to zaimplementować w przypadku modułów JavaScript, gdy są one używane przez usługę powiązaną. Ponadto w niektórych przypadkach podczas sprawdzania aktualizacji skrypty muszą mieć możliwość ominięcia pamięci podręcznej.

Dodatkowe materiały i informacje