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.