Articles

Het web threaden met modulewerkers

JavaScript is single-threaded, wat betekent dat het slechts één bewerking tegelijk kan uitvoeren. Dit is intuïtief en werkt goed voor veel gevallen op het web, maar kan problematisch worden wanneer we zware hijstaken zoals gegevensverwerking, parsing, berekening of analyse moeten doen. Naarmate meer en meer complexe toepassingen worden geleverd op het web, is er een toenemende behoefte aan multi-threaded verwerking.

op het webplatform is de belangrijkste primitief voor threading en parallellisme de Web Workers API. Werknemers zijn een lichtgewicht abstractie op de top van het besturingssysteem threads die een bericht passeren API voor inter-thread communicatie bloot. Dit kan enorm nuttig zijn bij het uitvoeren van dure berekeningen of het werken op grote datasets, waardoor de hoofdthread soepel kan draaien tijdens het uitvoeren van de dure bewerkingen op een of meer achtergrondthreads.

Hier is een typisch voorbeeld van worker-gebruik, waarbij een worker script luistert naar berichten van de hoofdthread en reageert door eigen berichten terug te sturen:

pagina.js:

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

werknemer.js:

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

de web Worker API is al meer dan tien jaar beschikbaar in de meeste browsers. Terwijl dat betekent dat werknemers hebben uitstekende browser ondersteuning en zijn goed geoptimaliseerd, het betekent ook dat ze lang vóór Javascript modules. Aangezien er geen module systeem toen werknemers werden ontworpen, de API voor het laden van code in een werknemer en het samenstellen van scripts is vergelijkbaar met de synchrone script Laden benaderingen gebruikelijk in 2009 gebleven.

geschiedenis: classic workers #

De Arbeidersconstructor neemt een Classic script URL, die relatief is aan de document URL. Het geeft onmiddellijk een verwijzing terug naar de nieuwe werknemer instantie, die een berichteninterface blootlegt, evenals een terminate() methode die de werknemer onmiddellijk stopt en vernietigt.

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

een importScripts() functie is beschikbaar binnen webwerkers voor het laden van extra code, maar het pauzeert de uitvoering van de werknemer om elk script op te halen en te evalueren. Het voert ook scripts uit in de Globale scope zoals een klassieke <script> tag, wat betekent dat de variabelen in een script kunnen worden overschreven door de variabelen in een ander script.

werknemer.js:

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

greet.js:

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

om deze reden hebben webwerkers historisch gezien een buitenproportioneel effect op de architectuur van een toepassing opgelegd. Ontwikkelaars hebben slimme tooling en workarounds moeten maken om het mogelijk te maken om webwerkers te gebruiken zonder moderne ontwikkelingspraktijken op te geven. Als voorbeeld, bundlers zoals webpack insluiten een kleine module loader implementatie in gegenereerde code die importScripts() gebruikt voor het laden van code, maar wikkelt modules in functies om variabele botsingen te voorkomen en afhankelijkheid import en export simuleren.

Enter module workers #

een nieuwe modus voor webwerkers met de ergonomische en prestatievoordelen van JavaScript-modules wordt verzonden in Chrome 80, genaamd module workers. DeWorker constructor accepteert nu een nieuwe{type:"module"} optie, die het laden en uitvoeren van script verandert om overeen te komen met<script type="module">.

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

omdat modulewerkers standaard JavaScript-modules zijn, kunnen ze import-en exportverklaringen gebruiken. Zoals met alle JavaScript-modules, worden afhankelijkheden slechts eenmaal uitgevoerd in een bepaalde context (hoofdthread, worker, enz.), en alle toekomstige invoer verwijzen naar de reeds uitgevoerde module instantie. Het laden en uitvoeren van JavaScript-modules wordt ook geoptimaliseerd door browsers. De afhankelijkheden van een module kunnen worden geladen voordat de module wordt uitgevoerd, waardoor volledige modulebomen parallel kunnen worden geladen. Module laden caches ook parsed code, wat betekent dat modules die worden gebruikt op de belangrijkste thread en in een werknemer hoeft slechts eenmaal worden ontleed.

verplaatsen naar JavaScript-modules maakt ook het gebruik van dynamische import voor lazy-loading code mogelijk zonder de uitvoering van de werknemer te blokkeren. Dynamische import is veel explicieter dan het gebruik van importScripts() om afhankelijkheden te laden, omdat de exports van de geïmporteerde module worden geretourneerd in plaats van te vertrouwen op globale variabelen.

werknemer.js:

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

greet.js:

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

om goede prestaties te garanderen, is de oude importScripts() methode niet beschikbaar binnen modulewerkers. Het wisselen van werknemers om JavaScript-modules te gebruiken betekent dat alle code in strikte modus wordt geladen. Een andere opmerkelijke verandering is dat de waarde van this in de top-level scope van een JavaScript-module undefined is, terwijl bij klassieke werknemers de waarde het globale scope van de werknemer is. Gelukkig is er altijd een self global geweest die een verwijzing geeft naar het globale bereik. Het is beschikbaar in alle soorten werknemers, waaronder dienstverlenende werknemers, evenals in de DOM.

Module workers verwijderen ook ondersteuning voor HTML-stijl opmerkingen. Wist je dat je HTML-opmerkingen kon gebruiken in web worker scripts?

Preload workers with modulepreload #

een aanzienlijke prestatieverbetering die wordt geleverd met module workers is de mogelijkheid om werknemers en hun afhankelijkheden vooraf te laden. Bij module workers worden scripts geladen en uitgevoerd als standaard JavaScript-modules, wat betekent dat ze vooraf geladen en zelfs vooraf ontleed kunnen worden met 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>

voorgeladen modules kunnen ook gebruikt worden door zowel de hoofdthread als de module workers. Dit is handig voor modules die in beide contexten worden geïmporteerd, of in gevallen waarin het niet mogelijk is om van tevoren te weten of een module op de hoofdthread of in een werkername zal worden gebruikt.

voorheen waren de beschikbare opties voor het vooraf laden van webwerkscripts beperkt en niet noodzakelijk betrouwbaar. Klassieke werknemers hadden hun eigen “worker” brontype voor het vooraf laden, maar geen browsers geïmplementeerd <link rel="preload" as="worker">. Als gevolg hiervan was de primaire beschikbare techniek voor het vooraf laden van webwerkers het gebruik van <link rel="prefetch">, die volledig gebaseerd was op de HTTP-cache. Wanneer gebruikt in combinatie met de juiste caching headers, maakte dit het mogelijk om te voorkomen dat de werknemer instantiation moet wachten om het script te downloaden. Echter, in tegenstelling tot modulepreload ondersteunt deze techniek het vooraf laden van afhankelijkheden of het vooraf parsen niet.

hoe zit het met gedeelde werknemers? #

gedeelde werknemers zijn bijgewerkt met ondersteuning voor JavaScript-modules vanaf Chrome 83. Net als toegewijde werknemers laadt de optie {type:"module"} nu het werkerscript als een module in plaats van een klassiek script:

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

voordat JavaScript-modules werden ondersteund, verwachtte de SharedWorker() constructor alleen een URL en een optionele name argument. Dit zal blijven werken voor klassiek gedeeld gebruik door werknemers; voor het maken van module shared workers moet echter het nieuwe options argument worden gebruikt. De beschikbare opties zijn dezelfde als die voor een dedicated worker, inclusief dename optie die het vorigename argument vervangt.

hoe zit het met de dienstverlener? #

de service worker-specificatie is al bijgewerkt om het accepteren van een JavaScript-module als invoerpunt te ondersteunen, waarbij dezelfde {type:"module"} optie wordt gebruikt als module workers, maar deze wijziging moet nog worden geïmplementeerd in browsers. Zodra dat gebeurt, zal het mogelijk zijn om een servicewerker te installeren met behulp van een JavaScript-module met behulp van de volgende code:

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

nu de specificatie is bijgewerkt, beginnen browsers het nieuwe gedrag te implementeren. Dit kost tijd, omdat er een aantal extra complicaties in verband met het brengen van JavaScript modules naar service worker. Service worker registratie moet geïmporteerde scripts te vergelijken met hun vorige versies in de cache bij het bepalen of een update te activeren, en dit moet worden geïmplementeerd voor JavaScript-modules wanneer gebruikt voor service workers. Ook, service werknemers moeten in staat zijn om de cache te omzeilen voor scripts in bepaalde gevallen bij het controleren op updates.