Articles

Threading del web con i lavoratori del modulo

JavaScript è single-threaded, il che significa che può eseguire solo un’operazione alla volta. Questo è intuitivo e funziona bene per molti casi sul web, ma può diventare problematico quando abbiamo bisogno di fare attività di sollevamento pesante come l’elaborazione dei dati, l’analisi, il calcolo o l’analisi. Man mano che le applicazioni sempre più complesse vengono distribuite sul Web, aumenta la necessità di un’elaborazione multi-thread.

Sulla piattaforma web, la primitiva principale per il threading e il parallelismo è l’API Web Worker. I lavoratori sono un’astrazione leggera sopra i thread del sistema operativo che espongono un messaggio che passa API per la comunicazione inter-thread. Ciò può essere estremamente utile quando si eseguono calcoli costosi o si operano su set di dati di grandi dimensioni, consentendo al thread principale di funzionare senza problemi durante l’esecuzione di costose operazioni su uno o più thread in background.

Ecco un tipico esempio di utilizzo di worker, in cui uno script worker ascolta i messaggi dal thread principale e risponde inviando i propri messaggi:

pagina.js:

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

lavoratore.js:

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

L’API Web Worker è disponibile nella maggior parte dei browser da oltre dieci anni. Mentre ciò significa che i lavoratori hanno un eccellente supporto del browser e sono ben ottimizzati, significa anche che a lungo precedono i moduli JavaScript. Poiché non esisteva un sistema di moduli quando sono stati progettati i worker, l’API per il caricamento del codice in un worker e la composizione degli script è rimasta simile agli approcci di caricamento degli script sincroni comuni nel 2009.

Storia: lavoratori classici #

Il costruttore del lavoratore prende un URL di script classico, che è relativo all’URL del documento. Restituisce immediatamente un riferimento alla nuova istanza worker, che espone un’interfaccia di messaggistica e un metodoterminate() che interrompe immediatamente e distrugge il worker.

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

Una funzione importScripts() è disponibile all’interno dei web worker per caricare codice aggiuntivo, ma interrompe l’esecuzione del worker per recuperare e valutare ogni script. Esegue anche script nell’ambito globale come un classico tag<script>, il che significa che le variabili in uno script possono essere sovrascritte dalle variabili in un altro.

lavoratore.js:

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

salutare.js:

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

Per questo motivo, i web worker hanno storicamente imposto un effetto fuori misura sull’architettura di un’applicazione. Gli sviluppatori hanno dovuto creare strumenti intelligenti e soluzioni alternative per rendere possibile l’utilizzo di web worker senza rinunciare alle moderne pratiche di sviluppo. Ad esempio, i bundler come webpack incorporano una piccola implementazione del caricatore di moduli nel codice generato che utilizza importScripts() per il caricamento del codice, ma avvolge i moduli in funzioni per evitare collisioni variabili e simulare le importazioni e le esportazioni di dipendenze.

Inserisci module workers #

Una nuova modalità per i lavoratori web con i vantaggi ergonomici e prestazionali dei moduli JavaScript è disponibile in Chrome 80, chiamata module workers. Il costruttoreWorker ora accetta una nuova opzione{type:"module"}, che modifica il caricamento e l’esecuzione dello script in modo che corrisponda a<script type="module">.

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

Poiché i lavoratori dei moduli sono moduli JavaScript standard, possono utilizzare le istruzioni import ed export. Come con tutti i moduli JavaScript, le dipendenze vengono eseguite solo una volta in un dato contesto (thread principale, worker, ecc.), e tutte le importazioni future fanno riferimento all’istanza del modulo già eseguita. Anche il caricamento e l’esecuzione dei moduli JavaScript sono ottimizzati dai browser. Le dipendenze di un modulo possono essere caricate prima dell’esecuzione del modulo, il che consente di caricare interi alberi di moduli in parallelo. Il caricamento del modulo memorizza anche il codice analizzato, il che significa che i moduli utilizzati nel thread principale e in un lavoratore devono essere analizzati una sola volta.

Passare ai moduli JavaScript consente anche l’uso dell’importazione dinamica per il codice lazy-loading senza bloccare l’esecuzione del lavoratore. L’importazione dinamica è molto più esplicita dell’utilizzo di importScripts() per caricare le dipendenze, poiché le esportazioni del modulo importato vengono restituite piuttosto che basarsi su variabili globali.

lavoratore.js:

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

salutare.js:

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

Per garantire grandi prestazioni, il vecchio importScripts() metodo non è disponibile all’interno dei lavoratori del modulo. Cambiare i lavoratori per utilizzare i moduli JavaScript significa che tutto il codice viene caricato in modalità rigorosa. Un altro cambiamento notevole è che il valore di this nell’ambito di primo livello di un modulo JavaScript è undefined, mentre nei lavoratori classici il valore è l’ambito globale del lavoratore. Fortunatamente, c’è sempre stato unself globale che fornisce un riferimento all’ambito globale. È disponibile in tutti i tipi di lavoratori, compresi i lavoratori del servizio, nonché nel DOM.

I lavoratori del modulo rimuovono anche il supporto per i commenti in stile HTML. Lo sapevate che è possibile utilizzare i commenti HTML negli script web worker?

Preload workers con modulepreload #

Un miglioramento sostanziale delle prestazioni che viene fornito con module workers è la capacità di precaricare i workers e le loro dipendenze. Con i lavoratori del modulo, gli script vengono caricati ed eseguiti come moduli JavaScript standard, il che significa che possono essere precaricati e persino pre-analizzati utilizzando 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>

I moduli precaricati possono essere utilizzati anche dal thread principale e dai lavoratori del modulo. Ciò è utile per i moduli importati in entrambi i contesti o nei casi in cui non è possibile sapere in anticipo se un modulo verrà utilizzato sul thread principale o in un worker.

In precedenza, le opzioni disponibili per il precaricamento degli script web worker erano limitate e non necessariamente affidabili. I lavoratori classici avevano il proprio tipo di risorsa “worker”per il precaricamento, ma nessun browser implementava <link rel="preload" as="worker">. Di conseguenza, la tecnica principale disponibile per il precaricamento dei web worker era quella di utilizzare <link rel="prefetch">, che si basava interamente sulla cache HTTP. Se usato in combinazione con le intestazioni di caching corrette, ciò ha permesso di evitare l’istanziazione del lavoratore che doveva attendere per scaricare lo script del lavoratore. Tuttavia, a differenza di modulepreload questa tecnica non supportava le dipendenze di precaricamento o la pre-analisi.

Che dire dei lavoratori condivisi? #

I lavoratori condivisi sono stati aggiornati con il supporto per i moduli JavaScript a partire da Chrome 83. Come dedicato lavoratori, la costruzione di un comune lavoratore, con il {type:"module"} opzione carica ora il lavoratore script come un modulo piuttosto che un classico script:

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

Prima del supporto di JavaScript moduli, il SharedWorker() costruttore previsto solo un URL e un opzionale name argomento. Questo continuerà a funzionare per il classico utilizzo del lavoratore condiviso; tuttavia, la creazione di lavoratori condivisi del modulo richiede l’utilizzo del nuovo argomento options. Le opzioni disponibili sono le stesse di un worker dedicato, inclusa l’opzionename che sostituisce il precedente argomentoname.

Che cosa circa il lavoratore di servizio? #

La specifica del service worker è già stata aggiornata per supportare l’accettazione di un modulo JavaScript come punto di ingresso, utilizzando la stessa opzione{type:"module"} come lavoratori del modulo, tuttavia questa modifica deve ancora essere implementata nei browser. Una volta che ciò accade, sarà possibile istanziare un service worker utilizzando un modulo JavaScript utilizzando il seguente codice:

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

Ora che la specifica è stata aggiornata, i browser stanno iniziando a implementare il nuovo comportamento. Questo richiede tempo perché ci sono alcune complicazioni extra associate al portare i moduli JavaScript a service worker. La registrazione del service worker deve confrontare gli script importati con le versioni precedenti memorizzate nella cache per determinare se attivare un aggiornamento e ciò deve essere implementato per i moduli JavaScript quando viene utilizzato per i service worker. Inoltre, i lavoratori del servizio devono essere in grado di bypassare la cache per gli script in alcuni casi durante il controllo degli aggiornamenti.