Spostare le operazioni più complesse nei thread in background è ora più semplice con i moduli JavaScript nei web worker.
JavaScript è single-threaded, il che significa che può eseguire una sola operazione alla volta. Questo è intuitivo e funziona bene per molti casi sul web, ma può diventare problematico quando dobbiamo eseguire attività impegnative come il trattamento dati, l'analisi, il calcolo o l'analisi dei dati. Man mano che sul web vengono pubblicate applicazioni sempre più complesse, aumenta la necessità di 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 dei thread del sistema operativo che espongono un'API di passaggio dei 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 costose su uno o più thread in background.
Ecco un esempio tipico di utilizzo dei worker, in cui uno script worker rimane in ascolto dei messaggi provenienti dal 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. Ciò significa che i worker hanno un eccellente supporto del browser e sono ben ottimizzati, ma anche che sono molto precedenti ai moduli JavaScript. Poiché non esisteva un sistema di moduli quando sono stati progettati i 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: worker classici
Il costruttore Worker accetta un URL di uno script classico, che è relativo all'URL del documento. Restituisce immediatamente un riferimento alla nuova istanza worker, che espone un'interfaccia di messaggistica e un metodo terminate() che arresta 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 mette in pausa l'esecuzione del worker per recuperare e valutare ogni script. Inoltre, esegue gli script nell'ambito globale come un tag <script> classico, il che significa che le variabili in uno script possono essere sovrascritte dalle variabili in 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 avuto un impatto notevole sull'architettura di un'applicazione. Gli sviluppatori hanno dovuto creare strumenti e soluzioni intelligenti per rendere possibile l'utilizzo dei 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 racchiude i moduli in funzioni per evitare collisioni di variabili e simulare importazioni ed esportazioni di dipendenze.
Inserisci i worker del modulo
In Chrome 80 è disponibile una nuova modalità per i web worker con i vantaggi ergonomici e prestazionali dei moduli JavaScript, chiamati worker dei moduli. Il costruttore Worker ora accetta una nuova opzione {type:"module"}, che modifica il caricamento e l'esecuzione degli 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 interi alberi di moduli in parallelo. 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 anche l'utilizzo dell'importazione dinamica per il codice di caricamento lento 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 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 prestazioni ottimali, il vecchio metodo importScripts() non è disponibile nei worker dei moduli. Il passaggio dei worker all'utilizzo dei moduli JavaScript comporta il caricamento di tutto il codice in modalità StrictMode. Un'altra modifica importante è 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, esiste sempre 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 miglioramento significativo 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 preanalizzati 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 worker dei moduli. 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 dei web worker 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 funzionalità ha consentito di evitare che l'inizializzazione del worker dovesse attendere il download dello script del worker. Tuttavia, a differenza di modulepreload, questa tecnica non supportava il precaricamento delle dipendenze o l'analisi preliminare.
E i worker condivisi?
I worker condivisi sono stati aggiornati con il supporto dei moduli JavaScript a partire da Chrome 83. Come per gli worker dedicati,
la creazione di uno shared worker 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 dei moduli richiede l'utilizzo del nuovo argomento options. Le opzioni disponibili sono le stesse di un worker dedicato, inclusa l'opzione name che sostituisce l'argomento name precedente.
E il service worker?
La specifica del service worker è già stata aggiornata per supportare l'accettazione di un modulo JavaScript come entry point, utilizzando la stessa opzione {type:"module"} dei worker dei moduli, ma questa modifica non è ancora stata implementata nei browser. Una volta che ciò accade, 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. Questa operazione richiede tempo perché l'introduzione dei moduli JavaScript nel service worker comporta alcune complicazioni aggiuntive. La registrazione del service worker deve confrontare gli script importati con le versioni memorizzate nella cache precedenti per determinare se attivare un aggiornamento. Questa operazione deve essere implementata per i moduli JavaScript quando vengono utilizzati per i service worker. Inoltre, i service worker devono essere in grado di bypassare la cache per gli script in determinati casi durante la ricerca di aggiornamenti.