JavaScript Web Workers
Scopri i Web Workers JavaScript per eseguire codice in un thread in background — mantieni l'interfaccia reattiva, comunica con postMessage e trasferisci dati con gli oggetti trasferibili.
JavaScript esegue il codice su un singolo thread principale — lo stesso thread che dispone la pagina, disegna i pixel e gestisce clic e tastiera. Quel singolo thread è il cuore dell'event loop: preleva un'attività, la esegue fino al completamento e poi passa alla successiva. Quindi quando una funzione fa qualcosa di genuinamente pesante — elaborare un grande array, analizzare un file da diversi megabyte, calcolare l'hash di una password migliaia di volte — il loop rimane bloccato all'interno di quella funzione. Non può succedere nient'altro: lo scorrimento si blocca, i pulsanti smettono di rispondere, la pagina sembra congelata fino al termine del lavoro.
I Web Workers risolvono questo problema eseguendo uno script su un thread in background separato, in parallelo con il thread principale. Il lavoro pesante si sposta fuori dal percorso critico, l'interfaccia rimane reattiva e i due thread comunicano tra loro passando messaggi.
Perché i Timer Non Bastano
Un primo istinto comune è quello di racchiudere il lavoro lento in un setTimeout sperando che venga eseguito "in background". Non è così. I timer si limitano a differire un'attività a un turno successivo dello stesso loop — quando il callback viene infine chiamato, viene ancora eseguito sul thread principale e blocca comunque tutto durante l'esecuzione. (Vedi scheduling con setTimeout e setInterval per capire come funziona davvero quella coda.)
// This still freezes the page — it just freezes it 50ms later.
setTimeout(() => {
let total = 0;
for (let i = 0; i < 5_000_000_000; i++) total += i;
console.log(total);
}, 50);Un Web Worker è diverso: il suo codice viene eseguito su un thread completamente diverso, quindi il thread principale è libero di continuare a renderizzare e rispondere mentre il worker elabora.
Creare un Worker
Un worker è un file JavaScript separato. Lo si crea puntando il costruttore Worker all'URL di quel file:
const worker = new Worker('worker.js');Il browser avvia un nuovo thread, scarica worker.js e inizia a eseguirlo. Da quel momento in poi, i due script comunicano solo tramite messaggi — non condividono variabili, funzioni né oggetti.
Ecco la coppia di file più piccola e completa.
main.js
const worker = new Worker('worker.js');
worker.postMessage('Hello from the main thread');
worker.onmessage = (event) => {
console.log('Main received:', event.data);
};worker.js
self.onmessage = (event) => {
console.log('Worker received:', event.data);
self.postMessage('Hello back from the worker');
};Messaggistica con postMessage
La comunicazione è bidirezionale e asincrona. Il thread principale chiama worker.postMessage(data) e ascolta con worker.onmessage; all'interno del worker, self.postMessage(data) invia una risposta e self.onmessage riceve. Ogni handler riceve un MessageEvent e il payload si trova nella sua proprietà .data.
I dati inviati vengono copiati, non condivisi, tramite l'algoritmo di clone strutturato. Ciò significa che è possibile passare stringhe, numeri, booleani, array, object semplici, Map, Set, Date, ArrayBuffer e molto altro — ma non funzioni, nodi DOM o istanze di classi con metodi. Poiché si tratta di una copia, modificare un object da un lato non influisce mai sull'altro.
Ecco un ciclo completo di andata e ritorno. Il worker calcola il numero di Fibonacci all'n-esima posizione con l'algoritmo ricorsivo ingenuo — deliberatamente lento, esattamente il tipo di lavoro che provocherebbe rallentamenti all'interfaccia se venisse eseguito sul thread principale.
main.js
const worker = new Worker('worker.js');
worker.onmessage = (event) => {
console.log(`fib(${event.data.n}) = ${event.data.result}`);
};
// The page stays interactive while this runs on the worker thread.
worker.postMessage({ n: 42 });worker.js
function fib(n) {
return n < 2 ? n : fib(n - 1) + fib(n - 2);
}
self.onmessage = (event) => {
const { n } = event.data;
const result = fib(n);
self.postMessage({ n, result });
};Il thread principale invia la richiesta e torna immediatamente a gestire clic e rendering. Quando il worker termina, il risultato arriva come messaggio — nessun blocco, nessuno stuttering dello spinner.
Il Global Scope del Worker
All'interno di un worker non esiste window. L'object globale è self (un DedicatedWorkerGlobalScope) e, cosa fondamentale, non esistono né document né il DOM. Un worker non può leggere o modificare la pagina; se ha bisogno che l'interfaccia venga aggiornata, invia un messaggio e lascia che sia il thread principale a farlo.
Il codice all'interno di un Web Worker non può toccare il DOM. Non esiste né document, né window, né accesso agli elementi della pagina. Qualsiasi cosa visiva deve essere restituita al thread principale tramite postMessage. Questa restrizione è ciò che rende sicura l'esecuzione parallela dei worker — non c'è uno stato condiviso dell'interfaccia che possa essere corrotto.
I worker sono tutt'altro che limitati, però. Il worker scope mette a disposizione molte API utili:
importScripts('a.js', 'b.js')per caricare script classici in modo sincrono.fetcheXMLHttpRequestper le richieste di rete.- Timer:
setTimeout,setInterval. console,crypto,TextEncoder/TextDecoder,WebSocket, IndexedDB e molto altro.
Questo rende i worker un'ottima sede per recupero di dati dalla rete, parsing, compressione e crittografia — lavoro autocontenuto che produce un risultato da restituire alla pagina.
Gestione degli Errori e Terminazione
Se un worker genera un errore non gestito, questo emerge nel thread principale tramite worker.onerror:
worker.onerror = (event) => {
console.error(`Worker error: ${event.message} (${event.filename}:${event.lineno})`);
};Un worker continua a girare finché non viene fermato. Puoi fermarlo dall'esterno con worker.terminate(), che termina immediatamente il thread — qualsiasi lavoro in corso viene abbandonato:
worker.terminate();Oppure il worker può chiudersi autonomamente dall'interno una volta terminato:
// inside worker.js
self.close();Terminare i worker inattivi libera memoria; i worker a lunga vita che gestiscono molti messaggi possono tranquillamente essere mantenuti attivi.
Oggetti Trasferibili: Spostare Invece di Copiare
Il clone strutturato è comodo, ma copia i dati. Per un payload binario di grandi dimensioni — ad esempio un buffer immagine da 50 MB — copiare entrambi i lati spreca memoria e richiede tempo. Gli oggetti trasferibili consentono invece di cedere la proprietà: i dati vengono spostati nell'altro thread senza alcuna copia e il mittente perde l'accesso ad essi.
Lo si attiva passando un secondo argomento a postMessage — un elenco degli oggetti da trasferire:
const buffer = new ArrayBuffer(64 * 1024 * 1024); // 64 MB
// Transfer ownership of the buffer to the worker (no copy).
worker.postMessage({ buffer }, [buffer]);
console.log(buffer.byteLength); // 0 — this thread can no longer use itDopo il trasferimento, buffer.byteLength è 0 sul lato del mittente: la memoria appartiene ora al worker. Il trasferimento è ideale per gli ArrayBuffer e i typed array basati su di essi — vedi ArrayBuffer e array binari per capire come sono strutturati quei dati binari. Altri trasferibili includono MessagePort, ImageBitmap e OffscreenCanvas.
Il contenuto del secondo argomento deve essere presente anche nel messaggio. In worker.postMessage({ buffer }, [buffer]), il buffer è referenziato dal payload ed è elencato come trasferibile. Se si elenca qualcosa che non è raggiungibile dal messaggio, il browser genera un DataCloneError.
Worker come Moduli
Per impostazione predefinita, un worker è uno script classico, quindi usa importScripts() invece della sintassi dei moduli ES. Passando { type: 'module' }, il worker diventa un modulo e può usare import statico e dinamico:
const worker = new Worker('worker.js', { type: 'module' });worker.js
import { compress } from './compression.js';
self.onmessage = (event) => {
self.postMessage(compress(event.data));
};I worker come moduli sono l'impostazione predefinita moderna per il nuovo codice — offrono import appropriati, modalità strict e un grafo delle dipendenze più pulito.
Altri Tipi di Worker
Un semplice new Worker(...) crea un worker dedicato: appartiene alla singola pagina che lo ha avviato. Esistono due tipi di worker correlati — ma distinti — che è importante saper distinguere:
SharedWorker— una singola istanza di worker condivisa tra più tab, finestre o iframe dalla stessa origine. Le pagine si connettono ad essa tramite unMessagePort, il che la rende utile per coordinare lo stato o una singola connessione di rete tra tab. Non è un worker dedicato più veloce; è un worker condiviso.- Service Worker — un worker speciale che agisce come proxy di rete, posizionandosi tra la pagina e la rete per abilitare caching, supporto offline e notifiche push. È guidato da eventi e persiste oltre il ciclo di vita della pagina. Si tratta di un compito diverso da quello di un worker dedicato, che serve a "eseguire questo calcolo fuori dal thread principale"; per approfondire questo aspetto, leggi Service Workers.
Regola pratica: usa un Web Worker dedicato per spostare il lavoro intensivo per la CPU fuori dal thread principale, un SharedWorker per condividere un worker tra i tab dello stesso sito, e un Service Worker per controllare le richieste di rete e creare app che funzionano offline.
Quando Usare i Web Workers
I Web Workers sono preziosi ogni volta che un'attività è legata alla CPU e sufficientemente lunga da essere percepita come rallentamento:
- Calcoli intensivi — fisica, analisi di dati, ordinamenti e aggregazioni di grandi dimensioni.
- Elaborazione di immagini e video, inclusa la manipolazione di pixel fuori dallo schermo.
- Parsing e compressione di file di grandi dimensioni (CSV, JSON, archivi).
- Crittografia — hashing e cifratura senza bloccare l'input.
- Elaborazione di grandi dataset prima di restituire un risultato compatto per il rendering.
Se il collo di bottiglia è l'attesa sulla rete piuttosto che il calcolo, di solito non hai bisogno di un worker — fetch è già asincrono e non bloccante. I worker brillano quando è la CPU stessa a tenere occupato il thread principale.