Ora è più facile spostare i compiti più impegnativi nei thread in background con i moduli JavaScript nei worker web.
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 l'elaborazione, l'analisi, il calcolo o l'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 basata sui thread del sistema operativo che espongono un'API di passaggio di messaggi per la comunicazione tra thread. Questo può essere estremamente utile quando si eseguono calcoli costosi o si opera su set di dati di grandi dimensioni, consentendo al thread principale di funzionare senza problemi durante l'esecuzione delle operazioni dispendiose 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 messaggi propri:
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é non esisteva un sistema di moduli al momento della progettazione dei worker, l'API per caricare il codice in un worker e comporre gli script è rimasta simile agli approcci di caricamento degli script sincroni comuni nel 2009.
Cronologia: utenti classici
Il costruttore di Worker accetta un URL di script classico, che è 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 worker web è disponibile una funzione importScripts()
per il caricamento di codice aggiuntivo, ma interrompe 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 hanno storicamente imposto un effetto eccessivo 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 i lavoratori 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 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é vengono restituite le esportazioni del modulo importato anziché fare affidamento sulle 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, è sempre stato presente un parametro 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 miglioramento sostanziale 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 worker condivisi sono stati aggiornati con il supporto dei 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. A questo punto, sarà possibile eseguire l'inizializzazione di un worker di servizio 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 relative 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.