// apis · Web Platform Advent #24
Che cos'è un Web Worker? Un secondo thread, e la copia che decide se conviene
Un Web Worker esegue il tuo script su un thread separato, così il lavoro pesante smette di bloccare l'interfaccia. A che cosa ha accesso e a che cosa no, perché postMessage copia invece di condividere, come trasferire un ArrayBuffer evita quella copia, e in che cosa differisce da un Service Worker.
JavaScript gira su un solo thread, e quel thread gestisce anche clic, scorrimento e rendering. Un calcolo lungo lì non rallenta la pagina, la ferma. Un Web Worker è la via d'uscita standard: uno script che gira su un thread separato e fa il lavoro costoso mentre l'interfaccia resta reattiva.
Che cos'è davvero un worker
Punti un worker verso un file di script. Avvia il proprio contesto JavaScript, con il suo ambito globale e il suo ciclo di eventi, e resta in esecuzione finché non lo termini o la pagina non sparisce.
// main.js
const worker = new Worker('./heavy.js', { type: 'module' });
worker.postMessage({ rows: 250000 });
worker.onmessage = (event) => {
render(event.data.total);
};
worker.onerror = (event) => console.error(event.message); Dentro il worker l'oggetto globale è self e non window, e la forma è la stessa da entrambe le parti: ascoltare i messaggi, fare il lavoro, rimandare il risultato.
// heavy.js
self.onmessage = (event) => {
let total = 0;
for (let i = 0; i < event.data.rows; i++) total += compute(i);
self.postMessage({ total });
}; Niente di tutto questo è asincrono dentro il worker. Il ciclo blocca esattamente come bloccherebbe altrove; blocca solo un thread che nessuno sta guardando.
Niente DOM, ed è tutto il progetto
Un worker non ha window, non ha document e non ha alcun modo di toccare un elemento. Non è una dimenticanza. Il DOM non è pensato per essere manipolato da più thread, e darne accesso a due thread produrrebbe esattamente le corse critiche che la piattaforma vuole evitare.
Quello che ottiene, invece, è quasi tutto il resto: fetch, WebSocket, IndexedDB, i timer, crypto e importScripts per i worker classici. Un worker può quindi scaricare, analizzare, calcolare e memorizzare. Semplicemente non può mostrare nulla, ed è per questo che ogni risultato deve tornare al thread principale per essere reso.
Parlargli: postMessage copia, il trasferimento sposta
I messaggi non sono riferimenti condivisi. Il valore inviato viene duplicato dall'algoritmo di clonazione strutturata, lo stesso usato da IndexedDB, e le due parti finiscono con copie indipendenti. Funzioni e nodi del DOM non sono clonabili e sollevano un errore.
Quella copia è il costo nascosto, ed è dove il codice ingenuo perde. Spedire sessanta megabyte di array tipizzato significa serializzare e duplicare sessanta megabyte, il che può facilmente superare il tempo che il calcolo ha fatto risparmiare.
La via d'uscita è trasferire anziché copiare. Passa il buffer come secondo argomento e la sua proprietà si sposta al worker invece di essere duplicata; chi invia resta con un buffer vuoto, ed è proprio quello lo scopo.
const buffer = new ArrayBuffer(64 * 1024 * 1024);
// Copied: the main thread keeps its own version.
worker.postMessage(buffer);
// Transferred: ownership moves, nothing is copied,
// and buffer.byteLength becomes 0 on this side.
worker.postMessage(buffer, [buffer]); I trasferibili spiegano perché l'elaborazione di immagini e l'audio funzionano bene nei worker: i pixel si spostano una volta, non vengono copiati due volte.
Web Worker, Shared Worker, Service Worker
Tre cose portano la parola worker senza essere intercambiabili, ed è lì che sta gran parte della confusione.
Un worker dedicato, quello descritto qui, appartiene alla sola pagina che lo ha creato e muore con essa. Uno shared worker è raggiungibile da più pagine della stessa origine, utile per condividere una connessione fra schede. Un service worker è tutt'altra cosa: si mette fra la pagina e la rete come un proxy, funziona a eventi, il browser lo avvia e lo ferma a piacere, ed è ciò che rende possibili il comportamento offline e le notifiche push. Lo trattiamo a parte in che cos'è un service worker.
La regola breve: un web worker per togliere calcolo dal thread principale, un service worker per controllare che cosa succede alle richieste di rete. Prendere un service worker per accelerare un calcolo è un errore di categoria.
Quando conviene e quando no
Un worker si ripaga quando il lavoro è davvero pesante e i dati che attraversano il confine sono piccoli in confronto. Analizzare un CSV grande, indicizzare testo per una ricerca, comprimere, calcolare hash, decodificare immagini: tutto questo calcola molto e restituisce poco.
Non si ripaga quando l'attività è piccola, perché avviare un worker significa avviare un contesto JavaScript e non è gratis, né quando i dati sono enormi e il calcolo banale, perché il risparmio se ne andrà nella copia. Misura prima di supporre: il costo del confine è la parte che si dimentica.
Se sei qui per un punteggio di performance e non per una funzionalità lenta precisa, nota che questa è esattamente la leva dietro lo spostare lavoro fuori dal thread principale, cosa che il ciclo di eventi rende inevitabile in partenza.