Il browser esegue JavaScript, calcolo degli stili, layout e disegno su un unico thread principale. Finché il codice è leggero non c'è problema, ma quando una pagina deve elaborare un file CSV di migliaia di righe, filtrare un grande elenco a ogni tasto premuto, comprimere un'immagine o eseguire un algoritmo di ricerca, il main thread si blocca e l'interfaccia smette di rispondere.
Spezzare il lavoro in piccoli blocchi aiuta, ma non riduce la quantità totale di calcolo. I web worker offrono una via diversa: eseguono JavaScript in un thread separato, lasciando il main thread libero di rispondere all'utente.
Cosa può fare un worker e cosa no
Un worker è un ambiente JavaScript isolato. Ha a disposizione molte API utili, ma non tutte:
| Disponibile nel worker | Non disponibile nel worker |
|---|---|
fetch, WebSocket |
Accesso al DOM |
| IndexedDB, Cache Storage | window e document |
| Timer, Promise, moduli ES | localStorage |
| Crittografia (Web Crypto) | Manipolazione diretta della pagina |
OffscreenCanvas |
Eventi dell'interfaccia |
La regola pratica: il worker calcola, il main thread mostra. Tutto ciò che riguarda l'aspetto della pagina resta sul main thread; tutto ciò che è puro elaborare dati può spostarsi.
Un esempio minimo
Creare un worker richiede un file separato. Con i moduli ES la sintassi è moderna e pulita:
// main.js
const worker = new Worker(new URL('./filtro.worker.js', import.meta.url),
{ type: 'module' });
campo.addEventListener('input', () => {
worker.postMessage({ testo: campo.value });
});
worker.addEventListener('message', (e) => {
mostraRisultati(e.data.risultati);
});
// filtro.worker.js
let catalogo = [];
fetch('/dati/catalogo.json').then(r => r.json()).then(d => { catalogo = d; });
self.addEventListener('message', (e) => {
const q = e.data.testo.toLowerCase();
const risultati = catalogo.filter(p => p.nome.toLowerCase().includes(q))
.slice(0, 50);
self.postMessage({ risultati });
});
L'utente digita, il main thread invia il testo e resta libero; il worker filtra e restituisce solo i primi risultati da disegnare. Anche se il catalogo è grande, la digitazione non si blocca.
Il costo della comunicazione
I messaggi tra main thread e worker vengono copiati con l'algoritmo di clonazione strutturata. Per oggetti piccoli il costo è trascurabile, ma copiare grandi strutture a ogni messaggio può annullare il vantaggio. Ci sono tre strategie per limitarlo:
- tenere i dati nel worker: come nell'esempio, il catalogo si carica una volta dentro il worker e viaggiano solo la ricerca e un risultato ridotto;
- oggetti trasferibili: un
ArrayBufferpuò essere trasferito invece che copiato, passando la proprietà al worker in tempo quasi nullo. Dopo il trasferimento non è più utilizzabile dal mittente; - inviare meno: restituire solo ciò che serve a disegnare, non l'intero insieme elaborato.
const buffer = await file.arrayBuffer();
worker.postMessage({ buffer }, [buffer]); // trasferito, non copiato
Lavori adatti a un worker
Nel mio lavoro i casi in cui un worker fa davvero la differenza sono sempre di questo tipo:
- analisi di file caricati dall'utente: CSV, JSON di grandi dimensioni, log;
- ricerca e ordinamento su elenchi lunghi lato client;
- elaborazione di immagini: ridimensionamento, filtri, conversione di formato tramite
OffscreenCanvas; - compressione e decompressione di dati;
- evidenziazione della sintassi o formattazione di testi lunghi;
- calcoli numerici: statistiche, simulazioni, grafici con molti punti.
Non conviene invece spostare in un worker operazioni brevi: creare il worker e scambiare messaggi ha un costo, e per lavori di pochi millisecondi il guadagno è nullo.
Rendere la comunicazione più comoda
Gestire messaggi ed eventi a mano diventa scomodo quando le funzioni sono molte. Librerie come Comlink trasformano il worker in un oggetto con metodi asincroni: dal main thread si chiama await api.filtra(testo) come se fosse una funzione locale, e la libreria gestisce i messaggi. È una comodità, non un requisito: per uno o due tipi di messaggio il codice manuale resta chiaro.
Errori comuni
- Dimenticare la gestione degli errori: un'eccezione nel worker non appare nel flusso del main thread. Ascolta l'evento
errore, nei messaggi, restituisci uno stato esplicito. - Creare un worker per ogni operazione: crealo una volta e riutilizzalo.
- Risposte fuori ordine: se l'utente digita velocemente, una risposta vecchia può arrivare dopo una nuova. Associa a ogni richiesta un identificativo e ignora le risposte superate.
- Aspettarsi accesso al DOM: se il codice tocca
document, va diviso in una parte di calcolo e una di presentazione.
Verificare il beneficio
Nel pannello Performance di DevTools i worker compaiono come tracce separate. Prima della modifica vedrai task lunghi sul main thread durante l'operazione; dopo, il lavoro si sposta sulla traccia del worker e il main thread mostra solo brevi task di ricezione e disegno. Ripeti la misura con la CPU rallentata, per simulare un dispositivo di fascia bassa.
In sintesi
- Un web worker esegue JavaScript in un thread separato e libera il main thread.
- Il worker elabora dati; il DOM e l'interfaccia restano sul main thread.
- Tieni i dati grandi nel worker e usa gli oggetti trasferibili per i buffer.
- È utile per analisi di file, ricerche, immagini e calcoli, non per operazioni brevi.
- Gestisci errori e risposte fuori ordine, e verifica il risultato nel profilo.