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

Przenoszenie ciężkich zadań do wątków w tle jest teraz łatwiejsze dzięki modułom JavaScriptu w instancjach roboczych.

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 być problematyczne, gdy musimy wykonywać skomplikowane zadania, takie jak przetwarzanie danych, analizowanie, obliczanie lub analiza. W internecie pojawia się coraz więcej złożonych aplikacji, co zwiększa zapotrzebowanie na przetwarzanie wielowątkowe.

Na platformie internetowej głównym elementem podstawowym do obsługi wątków i równoległości jest interfejs Web Workers API. Procesy robocze to lekka abstrakcja wątków systemu operacyjnego, która udostępnia interfejs API do przekazywania wiadomości na potrzeby komunikacji między wątkami. Może to być bardzo przydatne podczas wykonywania kosztownych obliczeń lub operacji na dużych zbiorach danych, ponieważ pozwala na płynne działanie głównego wątku podczas wykonywania kosztownych operacji w co najmniej jednym wątku w tle.

Oto typowy przykład użycia skryptu instancji roboczej, który 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 skrypty service worker mają doskonałą obsługę przeglądarek i są dobrze zoptymalizowane, ale też, że powstały na długo przed modułami JavaScript. Ponieważ w czasie projektowania skryptów service worker nie było systemu modułów, interfejs API do wczytywania kodu do skryptu service worker i tworzenia skryptów pozostał podobny do synchronicznych metod wczytywania skryptów, które były powszechne 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 skryptu, która udostępnia interfejs do obsługi wiadomości oraz metodę terminate(), która natychmiast zatrzymuje i niszczy skrypt.

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

W procesach roboczych sieci Web dostępna jest funkcja importScripts(), która umożliwia wczytywanie dodatkowego kodu, ale wstrzymuje wykonywanie procesu roboczego w celu pobrania i oceny każdego skryptu. 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 skrypty web worker miały w przeszłości ogromny wpływ na architekturę aplikacji. Deweloperzy musieli tworzyć sprytne narzędzia i obejścia, aby móc korzystać z procesów roboczych bez rezygnowania z nowoczesnych metod programowania. Na przykład pakiety takie jak webpack umieszczają w wygenerowanym kodzie małą implementację modułu ładującego, która używa importScripts() do ładowania kodu, ale opakowuje moduły w funkcje, aby uniknąć kolizji zmiennych i symulować importowanie i eksportowanie zależności.

Wpisywanie instancji roboczych modułu

W Chrome 80 wprowadzamy nowy tryb dla skryptów web worker, który zapewnia ergonomię i wydajność modułów JavaScript. Nazywa się on skryptami module worker. Konstruktor Worker akceptuje teraz nową opcję {type:"module"}, która zmienia wczytywanie i wykonywanie skryptu, aby dopasować go do <script type="module">.

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

Ponieważ moduły robocze są standardowymi modułami JavaScript, mogą 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 (główny wątek, instancja robocza itp.), a wszystkie przyszłe importy odwołują się do już wykonanej instancji modułu. Przeglądarki optymalizują też wczytywanie i wykonywanie modułów JavaScriptu. Zależności modułu można wczytać przed jego wykonaniem, co umożliwia równoległe wczytywanie całych drzew modułów. Wczytywanie modułu powoduje też zapisanie przeanalizowanego kodu w pamięci podręcznej, co oznacza, że moduły używane w głównym wątku i w instancji roboczej muszą zostać przeanalizowane tylko raz.

Przejście na moduły JavaScriptu umożliwia też korzystanie z importu dynamicznego do leniwego ładowania kodu bez blokowania wykonywania skryptu service worker. Import dynamiczny jest znacznie bardziej jednoznaczny niż używanie importScripts() do wczytywania zależności, ponieważ zwracane są eksporty zaimportowanego modułu, a nie zmienne globalne.

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 skryptach service worker modułu. Przełączenie instancji roboczych na korzystanie z modułów JavaScriptu oznacza, że cały kod jest ładowany w trybie ścisłym. Inną istotną zmianą jest to, że wartość this w zakresie najwyższego poziomu modułu JavaScriptu to undefined, podczas gdy w klasycznych skryptach worker jest to zakres globalny skryptu worker. Na szczęście zawsze istniała zmienna globalna self, która odwołuje się do zakresu globalnego. Jest on dostępny we wszystkich typach skryptów, w tym w skryptach service worker, a także w DOM.

Wstępnie wczytaj instancje robocze za pomocą modulepreload

Jednym ze znaczących ulepszeń wydajności, które wprowadzają skrypty service worker modułu, jest możliwość wstępnego wczytywania skryptów service worker i ich zależności. W przypadku skryptów modułowych skrypty są wczytywane i wykonywane jako standardowe moduły JavaScriptu, co oznacza, że można je wstępnie wczytywać, a nawet wstępnie analizować 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ć też używane zarówno przez wątek główny, jak i przez skrypty modułów. Jest to przydatne w przypadku modułów importowanych w obu kontekstach lub w sytuacjach, gdy nie można z góry określić, czy moduł będzie używany w głównym wątku czy w procesie roboczym.

Wcześniej opcje wstępnego wczytywania skryptów skryptów web worker były ograniczone i nie zawsze niezawodne. Klasyczne skrypty service worker miały własny typ zasobu „worker” do wstępnego wczytywania, ale żadna przeglądarka nie implementowała <link rel="preload" as="worker">. W rezultacie główną techniką wstępnego wczytywania skryptów web worker było używanie <link rel="prefetch">, które opierało się całkowicie na pamięci podręcznej HTTP. W połączeniu z odpowiednimi nagłówkami buforowania pozwoliło to uniknąć sytuacji, w której instancja robocza musiała czekać na pobranie skryptu. Jednak w przeciwieństwie do modulepreload ta technika nie obsługiwała wstępnego wczytywania zależności ani wstępnego analizowania.

A co ze skryptami shared worker?

W Chrome 83 zaktualizowano współdzielone skrypty typu worker, dodając obsługę modułów JavaScript. Podobnie jak w przypadku dedykowanych instancji roboczych, utworzenie współdzielonej instancji roboczej z opcją {type:"module"} powoduje teraz wczytanie skryptu instancji roboczej jako modułu, a nie klasycznego skryptu:

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

Zanim wprowadzono obsługę modułów JavaScript, konstruktor SharedWorker() oczekiwał tylko adresu URL i opcjonalnego argumentu name. Będzie to nadal działać w przypadku klasycznego użycia udostępnionych skryptów, ale tworzenie udostępnionych skryptów modułów wymaga użycia nowego argumentu options. Dostępne opcje są takie same jak w przypadku dedykowanego procesu roboczego, w tym opcja name, która zastępuje poprzedni argument name.

A co ze skryptem service worker?

Specyfikacja skryptu service worker została już zaktualizowana, aby obsługiwać akceptowanie modułu JavaScript jako punktu wejścia, przy użyciu tej samej opcji {type:"module"} co w przypadku skryptów module worker, ale ta zmiana nie została jeszcze wdrożona w przeglądarkach. Gdy to nastąpi, będzie można utworzyć instancję skryptu service worker za pomocą modułu JavaScriptu, używając tego kodu:

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

Po zaktualizowaniu specyfikacji przeglądarki zaczynają wdrażać nowe zachowanie. Zajmuje to trochę czasu, ponieważ przeniesienie modułów JavaScriptu do skryptu service worker wiąże się z dodatkowymi komplikacjami. Podczas określania, czy należy aktywować aktualizację, rejestracja skryptu service worker musi porównać zaimportowane skrypty z ich poprzednimi wersjami w pamięci podręcznej. W przypadku modułów JavaScriptu używanych w skryptach service worker należy to zaimplementować. Ponadto skrypty service worker muszą mieć możliwość pominięcia pamięci podręcznej w przypadku skryptów w określonych sytuacjach podczas sprawdzania dostępności aktualizacji.

Dodatkowe materiały i informacje