Articles

Threading des Webs mit Modularbeitern

JavaScript ist Single-Threaded, was bedeutet, dass es jeweils nur eine Operation ausführen kann. Dies ist intuitiv und funktioniert gut für viele Fälle im Web, kann jedoch problematisch werden, wenn wir schwere Aufgaben wie Datenverarbeitung, Analyse, Berechnung oder Analyse ausführen müssen. Da immer komplexere Anwendungen im Web bereitgestellt werden, steigt der Bedarf an Multithread-Verarbeitung.

Auf der Webplattform ist das Hauptprimitiv für Threading und Parallelität die Web Workers-API. Worker sind eine leichte Abstraktion über Betriebssystemthreads, die eine Nachrichtenübermittlungs-API für die Kommunikation zwischen Threads verfügbar machen. Dies kann immens nützlich sein, wenn Sie kostspielige Berechnungen durchführen oder große Datensätze bearbeiten, sodass der Hauptthread reibungslos ausgeführt werden kann, während die teuren Vorgänge in einem oder mehreren Hintergrundthreads ausgeführt werden.

Hier ist ein typisches Beispiel für die Verwendung von Worker, bei dem ein Worker-Skript auf Nachrichten aus dem Hauptthread wartet und darauf antwortet, indem es eigene Nachrichten zurücksendet:

page .js:

const worker = new Worker('worker.js');
worker.addEventListener(e => {
console.log(e.data);
});
worker.postMessage('hello');

Arbeiter.js:

addEventListener('message', e => {
if (e.data === 'hello') {
postMessage('world');
}
});

Die Web Worker API ist in den meisten Browsern seit über zehn Jahren verfügbar. Das bedeutet zwar, dass sie über eine hervorragende Browserunterstützung verfügen und gut optimiert sind, bedeutet aber auch, dass sie JavaScript-Modulen lange voraus sind. Da es bei der Entwicklung von Workern kein Modulsystem gab, ist die API zum Laden von Code in einen Worker und zum Erstellen von Skripten ähnlich geblieben wie die im Jahr 2009 üblichen Ansätze zum synchronen Laden von Skripten.

History: classic workers #

Der Worker-Konstruktor verwendet eine klassische Skript-URL, die relativ zur Dokument-URL ist. Es gibt sofort einen Verweis auf die neue Worker-Instanz zurück, die eine Messaging-Schnittstelle sowie eine terminate() -Methode verfügbar macht, die den Worker sofort stoppt und zerstört.

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

Eine importScripts() -Funktion ist in Web Worker zum Laden von zusätzlichem Code verfügbar, pausiert jedoch die Ausführung des Workers, um jedes Skript abzurufen und auszuwerten. Es führt auch Skripte im globalen Bereich wie ein klassisches <script> -Tag aus, was bedeutet, dass die Variablen in einem Skript von den Variablen in einem anderen überschrieben werden können.

Arbeiter.js:

importScripts('greet.js');
// ^ could block for seconds
addEventListener('message', e => {
postMessage(sayHello());
});

grüßen.js:

// global to the whole worker
function sayHello() {
return 'world';
}

Aus diesem Grund haben Web Worker in der Vergangenheit einen übergroßen Effekt auf die Architektur einer Anwendung ausgeübt. Entwickler mussten clevere Werkzeuge und Problemumgehungen erstellen, um Web Worker verwenden zu können, ohne auf moderne Entwicklungspraktiken verzichten zu müssen. Als Beispiel betten Bundler wie Webpack eine kleine Modulladerimplementierung in generierten Code ein, der importScripts() zum Laden von Code verwendet, Module jedoch in Funktionen einschließt, um Variablenkollisionen zu vermeiden und Abhängigkeitsimporte und -exporte zu simulieren.

Enter module workers #

In Chrome 80 wird ein neuer Modus für Web Worker mit den Ergonomie- und Leistungsvorteilen von JavaScript-Modulen namens module Workers ausgeliefert. Der Worker -Konstruktor akzeptiert jetzt eine neue {type:"module"} -Option, die das Laden und Ausführen von Skripten an <script type="module"> anpasst.

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

Da Modularbeiter Standard-JavaScript-Module sind, können sie Import- und Exportanweisungen verwenden. Wie bei allen JavaScript-Modulen werden Abhängigkeiten in einem bestimmten Kontext (Hauptthread, Worker usw.) nur einmal ausgeführt.), und alle zukünftigen Importe verweisen auf die bereits ausgeführte Modulinstanz. Das Laden und Ausführen von JavaScript-Modulen wird auch von Browsern optimiert. Die Abhängigkeiten eines Moduls können vor der Ausführung des Moduls geladen werden, wodurch ganze Modulbäume parallel geladen werden können. Das Laden von Modulen speichert auch analysierten Code zwischen, was bedeutet, dass Module, die im Hauptthread und in einem Worker verwendet werden, nur einmal analysiert werden müssen.

Der Wechsel zu JavaScript-Modulen ermöglicht auch die Verwendung von dynamischem Import für Lazy-Loading-Code, ohne die Ausführung des Workers zu blockieren. Der dynamische Import ist viel expliziter als die Verwendung von importScripts() zum Laden von Abhängigkeiten, da die Exporte des importierten Moduls zurückgegeben werden, anstatt sich auf globale Variablen zu verlassen.

Arbeiter.js:

import { sayHello } from './greet.js';
addEventListener('message', e => {
postMessage(sayHello());
});

grüßen.js:

import greetings from './data.js';
export function sayHello() {
return greetings.hello;
}

Um eine hervorragende Leistung zu gewährleisten, ist die alte importScripts() Methode in Modularbeitern nicht verfügbar. Das Umschalten der Worker auf die Verwendung von JavaScript-Modulen bedeutet, dass der gesamte Code im strikten Modus geladen wird. Eine weitere bemerkenswerte Änderung ist, dass der Wert von this im Top-Level-Bereich eines JavaScript-Moduls undefined , während in klassischen Arbeitern der Wert der globale Bereich des Arbeiters ist. Glücklicherweise gab es immer einen self global , der einen Verweis auf den globalen Bereich bereitstellt. Es ist in allen Arten von Arbeitern verfügbar, einschließlich Servicemitarbeitern, sowie im DOM.

Modularbeiter entfernen auch die Unterstützung für Kommentare im HTML-Stil. Wussten Sie, dass Sie HTML-Kommentare in Web Worker-Skripten verwenden können?

Workers mit modulepreload #vorladen

Eine wesentliche Leistungsverbesserung, die mit Modul-Workern einhergeht, ist die Möglichkeit, Workers und ihre Abhängigkeiten vorzuladen. Dies bedeutet, dass sie mit modulepreloadvorgeladen und sogar vorab analysiert werden können:

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

Vorinstallierte Module können auch sowohl vom Hauptthread als auch von den Modularbeitern verwendet werden. Dies ist nützlich für Module, die in beiden Kontexten importiert werden, oder in Fällen, in denen nicht im Voraus bekannt ist, ob ein Modul im Hauptthread oder in einem Worker verwendet wird.

Bisher waren die verfügbaren Optionen zum Vorladen von Web Worker-Skripten begrenzt und nicht unbedingt zuverlässig. Klassische Worker hatten ihren eigenen „Worker“ -Ressourcentyp zum Vorladen, aber keine Browser implementiert <link rel="preload" as="worker">. Daher war die primäre Technik zum Vorladen von Web Workern die Verwendung von <link rel="prefetch"> , die sich vollständig auf den HTTP-Cache stützte. In Kombination mit den richtigen Caching-Headern konnte vermieden werden, dass die Worker-Instanziierung auf das Herunterladen des Worker-Skripts warten musste. Im Gegensatz zu modulepreload unterstützte diese Technik jedoch nicht das Vorladen von Abhängigkeiten oder das Vorparsen.

Was ist mit geteilten Arbeitern? #

Shared Worker wurden mit Unterstützung für JavaScript-Module ab Chrome 83 aktualisiert. Wie dedizierte Worker lädt das Erstellen eines gemeinsamen Workers mit der Option {type:"module"} das Worker-Skript jetzt als Modul und nicht als klassisches Skript:

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

Vor der Unterstützung von JavaScript-Modulen erwartete der Konstruktor SharedWorker() nur eine URL und eine optionale name Argument. Dies funktioniert weiterhin für die klassische Verwendung von Shared Worker; das Erstellen von freigegebenen Modularbeitern erfordert jedoch die Verwendung des neuen Arguments options . Die verfügbaren Optionen sind dieselben wie für einen dedizierten Worker, einschließlich der Option name, die das vorherige Argument name ersetzt.

Was ist mit dem Servicemitarbeiter? #

Die Service Worker-Spezifikation wurde bereits aktualisiert, um das Akzeptieren eines JavaScript-Moduls als Einstiegspunkt zu unterstützen, wobei dieselbe {type:"module"} -Option als Modul-Worker verwendet wird. Sobald dies geschieht, ist es möglich, einen Service Worker mithilfe eines JavaScript-Moduls mit dem folgenden Code zu instanziieren:

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

Nachdem die Spezifikation aktualisiert wurde, beginnen Browser, das neue Verhalten zu implementieren. Dies erfordert Zeit, da mit dem Bereitstellen von JavaScript-Modulen für Service Worker einige zusätzliche Komplikationen verbunden sind. Die Service-Worker-Registrierung muss importierte Skripte mit ihren vorherigen zwischengespeicherten Versionen vergleichen, um zu bestimmen, ob ein Update ausgelöst werden soll, und dies muss für JavaScript-Module implementiert werden, wenn es für Service-Worker verwendet wird. Außerdem müssen Servicemitarbeiter in der Lage sein, den Cache für Skripte in bestimmten Fällen zu umgehen, wenn sie nach Updates suchen.