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.

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. 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 wierzchu 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 na dużych zbiorach danych, ponieważ umożliwia płynne działanie wątku głównego przy wykonywaniu kosztownych operacji na co najmniej 1 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 pracownicy są doskonale obsługiwani przez przeglądarki i dobrze zoptymalizowani, 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');

Funkcja importScripts() jest dostępna w instancjach roboczych do wczytywania dodatkowego kodu, ale wstrzymuje jej wykonanie, 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ą 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. 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 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 umożliwia wczytywanie całych drzew modułów w tym samym czasie. Ł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 globalnych zmiennych.

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 moduły JavaScript oznacza, że cały kod jest ładowany 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ł element globalny self, który odwołuje 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 pracowników w modulepreload

Jednym z istotnych usprawnień wydajności, które zapewniają moduły workerów, jest możliwość wstępnego wczytania workerów i ich zależności. Moduły robocze 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>

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 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 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?

W Chrome 83 współdzielone instancje robocze obsługują moduły JavaScriptu. 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 zamiast 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. Ta metoda będzie nadal 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.

Co z skryptem service worker?

Specyfikacja service workera została już zaktualizowana, aby obsługiwać moduł JavaScriptu jako punkt wejścia, korzystając z tej samej opcji {type:"module"} co moduły instancji roboczej. Jednak 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. Jest to czasochłonne, ponieważ dodanie modułów JavaScript do skryptu service worker wiąże się z pewnymi dodatkowymi komplikacjami. 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