Spostare il lavoro pesante nei thread in background è ora più facile con i moduli JavaScript nei web worker.
JavaScript è a thread singolo, il che significa che può eseguire una sola operazione alla volta. Questo approccio è intuitivo e funziona bene per molti casi sul web, ma può diventare problematico quando dobbiamo svolgere attività complesse come elaborazione, analisi, calcolo o analisi dei dati. Man mano che vengono pubblicate applicazioni sempre più complesse 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 Workers. I worker sono un'astrazione leggera sopra i thread del sistema operativo che espongono un'API di trasmissione dei messaggi per le comunicazioni tra thread. Ciò può essere estremamente utile quando si eseguono calcoli costosi o si eseguono operazioni su set di dati di grandi dimensioni, consentendo al thread principale di funzionare senza problemi mentre si eseguono operazioni costose su uno o più thread in background.
Ecco un esempio tipico di utilizzo di un worker, in cui uno script worker ascolta i messaggi del thread principale e risponde inviando i propri messaggi:
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');
}
});
L'API Web Worker è disponibile nella maggior parte dei browser da oltre dieci anni. Sebbene questo significhi che i worker hanno un'eccellente assistenza per i browser e sono ben ottimizzati, significa anche che risalgono a molto prima dei moduli JavaScript. Poiché al momento della progettazione dei worker non esisteva un sistema a moduli, l'API per il caricamento del codice in un worker e la scrittura degli script è rimasta simile agli approcci al caricamento sincrono degli script comuni nel 2009.
Cronologia: utenti classici
Il costruttore worker accetta un URL di script classico relativo all'URL del documento. Restituisce immediatamente un riferimento alla nuova istanza di worker,
che espone un'interfaccia di messaggistica e un metodo terminate()
che interrompe e
distrugge immediatamente il worker.
const worker = new Worker('worker.js');
All'interno dei web worker è disponibile una funzione importScripts()
per caricare codice aggiuntivo, ma
sospende l'esecuzione del worker per recuperare e valutare ogni script. Inoltre, esegue gli script
in ambito globale come un tag <script>
classico, il che significa che le variabili di uno script possono essere
sovrascritte dalle variabili di un altro.
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';
}
Per questo motivo, i web worker storicamente hanno imposto un effetto enorme sull'architettura di un'applicazione. Gli sviluppatori hanno dovuto creare strumenti e soluzioni intelligenti per consentire di utilizzare i web worker senza rinunciare alle pratiche di sviluppo moderne. Ad esempio, i bundler come webpack incorporano un'implementazione di un piccolo caricatore di moduli nel codice generato che utilizza importScripts()
per il caricamento del codice, ma racchiude i moduli in funzioni per evitare collisioni di variabili e simulare le importazioni e le esportazioni delle dipendenze.
Inserisci worker del modulo
In Chrome 80 è disponibile una nuova modalità per i worker web con i vantaggi in termini di ergonomia e prestazioni dei moduli JavaScript, denominata worker dei moduli. Il
costruttore Worker
ora accetta una nuova opzione {type:"module"}
, che modifica il caricamento e
l'esecuzione dello script in modo che corrispondano a <script type="module">
.
const worker = new Worker('worker.js', {
type: 'module'
});
Poiché i worker dei moduli sono moduli JavaScript standard, possono utilizzare le istruzioni di importazione ed esportazione. Come per tutti i moduli JavaScript, le dipendenze vengono eseguite una sola volta in un determinato contesto (thread principale, worker e così via) 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 in parallelo interi alberi di moduli. Il caricamento dei moduli memorizza nella cache anche il codice analizzato, il che significa che i moduli utilizzati nel thread principale e in un worker devono essere analizzati una sola volta.
Il passaggio ai moduli JavaScript consente inoltre di utilizzare l'importazione dinamica per il codice di caricamento differito senza bloccare l'esecuzione del worker. L'importazione dinamica è molto più esplicita rispetto all'utilizzo di importScripts()
per caricare le dipendenze, poiché le esportazioni del modulo importato vengono restituite anziché fare affidamento su variabili globali.
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;
}
Per garantire ottime prestazioni, il vecchio metodo importScripts()
non è disponibile all'interno dei worker di modulo. Se configuri i worker in modo che utilizzino i moduli JavaScript, tutto il codice viene caricato in modalità rigorosa. Un'altra
variazione significativa è che il valore di this
nell'ambito di primo livello di un modulo JavaScript è
undefined
, mentre nei worker classici il valore è l'ambito globale del worker. Fortunatamente, c'è sempre stato un self
globale che fornisce un riferimento all'ambito globale. È disponibile in tutti i tipi di worker, inclusi i service worker, nonché nel DOM.
Precarica i worker con modulepreload
Un notevole miglioramento delle prestazioni offerto dai worker dei moduli è la possibilità di precaricare i worker e le relative dipendenze. Con i worker dei moduli, 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 pre caricati possono essere utilizzati anche dal thread principale e dai worker del modulo. Questo è utile per i moduli importati in entrambi i contesti o nei casi in cui non è possibile sapere in anticipo se un modulo verrà utilizzato nel thread principale o in un worker.
In precedenza, le opzioni disponibili per il precaricamento degli script di worker web erano limitate e non necessariamente affidabili. I worker classici avevano il proprio tipo di risorsa "worker" per il precaricamento, ma nessun browser ha implementato <link rel="preload" as="worker">
. Di conseguenza, la tecnica principale
disponibile per il precaricamento dei web worker era l'utilizzo di <link rel="prefetch">
, che si basava interamente sulla cache HTTP. Se utilizzata in combinazione con le intestazioni di memorizzazione nella cache corrette, questa operazione ha consentito di evitare di dover attendere il download dello script worker per l'inizializzazione del worker. Tuttavia, a differenza dimodulepreload
, questa tecnica non supportava il precaricamento delle dipendenze 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 per i worker dedicati,
la creazione di un worker condiviso con l'opzione {type:"module"}
ora carica lo script worker come
modulo anziché come script classico:
const worker = new SharedWorker('/worker.js', {
type: 'module'
});
Prima del supporto dei moduli JavaScript, il costruttore SharedWorker()
prevedeva solo un URL e un argomento name
facoltativo. Questo continuerà a funzionare per l'utilizzo dei worker condivisi classici, ma la creazione di worker condivisi del modulo richiede l'utilizzo del nuovo argomento options
. Le opzioni disponibili sono le stesse di quelle per un worker dedicato, inclusa l'opzione name
che sostituisce l'argomento name
precedente.
E i service worker?
La specifica del service worker è già stata aggiornata per supportare l'accettazione di un modulo JavaScript come punto di contatto, utilizzando la stessa opzione {type:"module"}
dei worker di modulo, tuttavia questa modifica deve ancora essere implementata nei browser. Sarà possibile creare un'istanza di un service worker utilizzando un modulo JavaScript con 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 aggiuntive associate al trasferimento dei moduli JavaScript ai worker di servizio. La registrazione dei worker di servizio deve confrontare gli script importati con le versioni memorizzate nella cache precedenti per determinare se attivare un aggiornamento e questo deve essere implementato per i moduli JavaScript quando vengono utilizzati per i worker di servizio. Inoltre, i worker di servizio devono essere in grado di aggirare la cache per gli script in alcuni casi durante la ricerca di aggiornamenti.