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

Przenoszenie wymagających obliczeń do wątków w tle jest teraz łatwiejsze dzięki modułom JavaScript w instancjach roboczych.

JavaScript jest językiem jednowątkowym, co oznacza, że może wykonywać tylko jedną operację naraz. Jest to intuicyjne i sprawdza się w wielu przypadkach w internecie, ale może stać się problematyczne, gdy musimy wykonywać wymagające zadania, takie jak przetwarzanie, analizowanie, obliczanie czy analiza danych. Coraz więcej złożonych aplikacji jest udostępnianych w internecie, dlatego rośnie zapotrzebowanie na przetwarzanie wielowątkowe.

Na platformie internetowej głównym elementem pierwotnym do obsługi wątków i równoległości jest Web Workers API. Workery to uproszczona abstrakcja wątków systemu operacyjnego, która udostępnia interfejs API do przekazywania wiadomości w celu komunikacji między wątkami. Może to być niezwykle przydatne podczas wykonywania kosztownych obliczeń lub operacji na dużych zbiorach danych, ponieważ wątek główny może działać płynnie, a kosztowne operacje są wykonywane w 1 lub większej liczbie wątków w tle.

Oto typowy przykład użycia skryptu roboczego, w którym skrypt 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 mają doskonałą obsługę przeglądarek i są dobrze zoptymalizowani, ale też znacznie wyprzedzają moduły JavaScript. Gdy projektowano instancje robocze, nie było systemu modułów, więc interfejs API do wczytywania kodu do instancji roboczej i tworzenia skryptów pozostał podobny do synchronicznych metod wczytywania skryptów popularnych w 2009 roku.

Historia: klasyczne instancje robocze

Konstruktor Worker przyjmuje adres URL klasycznego skryptu, który jest względny w stosunku do adresu URL dokumentu. Funkcja natychmiast zwraca odwołanie do nowej instancji procesu roboczego, która udostępnia interfejs przesyłania wiadomości oraz metodę terminate(), która natychmiast zatrzymuje i usuwa proces roboczy.

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

Funkcja importScripts() jest dostępna w wątkach roboczych do ładowania dodatkowego kodu, ale wstrzymuje wykonywanie wątku w celu pobrania i oceny każdego skryptu. Wykonuje też skrypty w zakresie globalnym, tak 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 internetowi od dawna mają ogromny wpływ na architekturę aplikacji. Deweloperzy musieli stworzyć sprytne narzędzia i obejścia, aby umożliwić korzystanie z procesów roboczych w internecie bez rezygnacji z nowoczesnych praktyk programistycznych. Na przykład programy pakujące, takie jak webpack, osadzają w wygenerowanym kodzie małą implementację modułu ładującego, która do ładowania kodu używa importScripts(), ale opakowuje moduły w funkcje, aby uniknąć kolizji zmiennych i symulować importowanie i eksportowanie zależności.

Wpisz instancje robocze 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 module workers. Konstruktor Worker akceptuje teraz nową opcję {type:"module"}, która zmienia wczytywanie i wykonywanie skryptu, aby pasowało 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 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ż wykonanej instancji modułu. Przeglądarki optymalizują też wczytywanie i wykonywanie modułów JavaScript. Zależności modułu można wczytać przed jego wykonaniem, co pozwala wczytywać całe drzewa modułów równolegle. Ładowanie modułów buforuje też przeanalizowany kod, co oznacza, że moduły używane w głównym wątku i w instancji roboczej muszą być analizowane tylko raz.

Przejście na moduły JavaScript umożliwia też korzystanie z dynamicznego importu do leniwego wczytywania kodu bez blokowania wykonywania procesu roboczego. 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 przypadku modułów roboczych. Przełączenie instancji roboczych na moduły JavaScript oznacza, że cały kod jest wczytywany w trybie ścisłym. Kolejną istotną zmianą jest to, że wartość this w zakresie najwyższego poziomu modułu JavaScriptu to undefined, podczas gdy w przypadku klasycznych procesów roboczych jest to zakres globalny procesu roboczego. Na szczęście zawsze istniała zmienna self globalna, która odnosi się do zakresu globalnego. Jest dostępny we wszystkich typach procesów roboczych, w tym w procesach roboczych usług, a także w DOM.

Wstępne wczytywanie instancji roboczych za pomocą modulepreload

Jednym z istotnych ulepszeń wydajności, które zapewniają moduły robocze, jest możliwość wstępnego wczytywania pracowników i ich zależności. W przypadku modułów roboczych skrypty są ładowane i wykonywane jako standardowe moduły JavaScript, co oznacza, że można je wstępnie wczytać, a nawet wstępnie 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ć też używane zarówno przez główny wątek, jak i przez instancje robocze modułów. Jest to przydatne w przypadku modułów importowanych w obu kontekstach lub w sytuacjach, w których 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 web worker były ograniczone i nie zawsze niezawodne. Klasyczne elementy robocze miały własny typ zasobu „worker” do wstępnego wczytywania, ale żadna przeglądarka nie implementowała <link rel="preload" as="worker">. Dlatego podstawową techniką wstępnego wczytywania procesów roboczych w internecie było używanie elementu <link rel="prefetch">, który w całości opierał się na pamięci podręcznej HTTP. W połączeniu z odpowiednimi nagłówkami pamięci podręcznej umożliwiało to uniknięcie sytuacji, w której tworzenie instancji roboczej musiało czekać na pobranie skryptu instancji roboczej. W przeciwieństwie domodulepreload ta technika nie obsługiwała jednak wstępnego wczytywania zależności ani wstępnego parsowania.

A co z pracownikami współdzielonymi?

Współdzielone procesy robocze zostały zaktualizowane w Chrome 83 i obsługują moduły JavaScript. Podobnie jak w przypadku dedykowanych instancji roboczych, utworzenie instancji roboczej z opcją {type:"module"} powoduje wczytanie 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 adresu URL i opcjonalnego argumentu name. Będzie to nadal działać w przypadku klasycznych udostępnionych procesów roboczych, ale tworzenie udostępnionych procesów roboczych 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 ze skryptem service worker?

Specyfikacja 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 instancji roboczych modułu. Ta zmiana nie została jeszcze wdrożona w przeglądarkach. Gdy to nastąpi, będzie można utworzyć instancję service workera 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. Wymaga to czasu, ponieważ wprowadzenie modułów JavaScript do komponentu Service Worker wiąże się z pewnymi dodatkowymi komplikacjami. Podczas określania, czy wywołać aktualizację, rejestracja service workera musi porównać zaimportowane skrypty z ich poprzednimi wersjami w pamięci podręcznej. Musi to być zaimplementowane w przypadku modułów JavaScript używanych w service workerach. Dodatkowo w niektórych przypadkach podczas sprawdzania aktualizacji service worker musi mieć możliwość pomijania pamięci podręcznej w przypadku skryptów.

Dodatkowe materiały i dalsze informacje