Diego Betto's Blog
Foto di sol

Diego Betto · 1 settembre 2026 · 4 min di lettura

Web Worker in JavaScript: come funzionano, quando (e come) usarli

Cos'è il main thread blocking, come creare un Web Worker per spostare calcoli pesanti fuori dalla UI, e quando invece non serve affatto.

Condividi:XLinkedInFacebookWhatsApp

JavaScript, nel browser, gira su un thread unico. Lo stesso thread che esegue il tuo codice disegna anche l’interfaccia, gestisce i click, fa scorrere la pagina. Finché il thread è libero, tutto sembra reattivo. Nel momento in cui gli dai un calcolo che richiede 300 millisecondi — parsare un JSON enorme, elaborare un’immagine, ordinare centinaia di migliaia di righe — quel thread smette di rispondere a qualunque altra cosa per tutta la durata del calcolo. Non un rallentamento: un blocco completo. Un click durante quei 300ms viene semplicemente ignorato finché il thread non torna libero.

Questo è il main thread blocking, ed è la causa più comune dietro un’interfaccia che sembra “impantanata” senza un errore evidente in console.

Cosa fa davvero un Web Worker

Un Web Worker esegue JavaScript su un thread separato, completamente isolato da quello principale. Non blocca la UI perché semplicemente non condivide il suo thread — gira in parallelo.

// worker.js — gira su un thread separato
self.onmessage = (e) => {
  const risultato = calcoloPesante(e.data);
  self.postMessage(risultato);
};

function calcoloPesante(numeri) {
  return numeri.reduce((acc, n) => acc + fattoriale(n), 0);
}

function fattoriale(n) {
  return n <= 1 ? 1 : n * fattoriale(n - 1);
}
// main.js — il thread principale, quello che disegna la UI
const worker = new Worker('worker.js');

worker.postMessage([50000, 60000, 70000]);

worker.onmessage = (e) => {
  console.log('Risultato:', e.data);
};

Il thread principale resta libero per tutta la durata del calcolo: l’interfaccia continua a rispondere ai click, lo scroll resta fluido, mentre il worker macina il risultato in background.

⚠ Attenzione

Un Worker non ha accesso al DOM — è il limite più importante da capire subito: dentro un Worker non esistono document, window né alcun elemento della pagina. Il Worker può solo ricevere dati, elaborarli, e restituire dati — non può leggere né modificare l’interfaccia direttamente. Ogni comunicazione passa da postMessage/onmessage, ed è per design: se un Worker potesse toccare il DOM, perderesti la garanzia di isolamento che lo rende sicuro da usare in parallelo.

Il costo nascosto: la copia dei dati

postMessage non condivide memoria tra i due thread — per default copia i dati passati (con l’algoritmo di structured clone). Per oggetti piccoli è irrilevante. Per un array di alcuni milioni di numeri, quella copia stessa può costare tempo non trascurabile, vanificando parte del beneficio.

La soluzione per dati grandi è Transferable: invece di copiare un ArrayBuffer, lo trasferisci — la proprietà passa al Worker e il thread principale perde l’accesso a quella memoria, senza che nessuna copia avvenga:

const buffer = new ArrayBuffer(1024 * 1024 * 50); // 50MB
worker.postMessage(buffer, [buffer]); // trasferito, non copiato — istantaneo

Dopo il trasferimento, buffer nel thread principale diventa inutilizzabile (byteLength torna 0) — è un vero passaggio di proprietà, non una condivisione.

Quando NON serve un Worker

Questa è la parte che la maggior parte delle guide salta, ed è quella che conta di più nella pratica: la maggioranza dei casi di UI lenta non è causata da un singolo calcolo pesante, ma da troppo lavoro DOM ripetuto — un querySelectorAll dentro un loop, un re-render React scatenato da un evento scroll senza throttle, un layout thrashing (leggere e scrivere proprietà di layout alternate in un ciclo). Un Worker non aiuta in nessuno di questi casi, perché il problema non è mai stato “troppo calcolo JavaScript puro” — è lavoro DOM che, per definizione, deve stare sul thread principale, Worker o no.

💡 Consiglio

Prima misura, poi ottimizza — non il contrario. Prima di introdurre un Worker, apri il profiler delle DevTools e guarda dove va davvero il tempo durante il blocco. Se il tempo è nel calcolo puro (un ciclo, un parsing, una trasformazione dati), un Worker è la risposta giusta. Se il tempo è in “Recalculate Style” o “Layout”, il problema è nel modo in cui il codice tocca il DOM — spesso risolvibile con debounce o throttle sull’handler che scatena il lavoro, non con un thread in più.

Un pattern pratico: il Worker come coda di lavoro

Per calcoli ricorrenti (non un singolo postMessage/onmessage), è comodo avvolgere il Worker in una piccola interfaccia a Promise:

class WorkerPool {
  #worker;
  #pending = new Map();
  #nextId = 0;

  constructor(scriptUrl) {
    this.#worker = new Worker(scriptUrl);
    this.#worker.onmessage = ({ data }) => {
      this.#pending.get(data.id)?.resolve(data.result);
      this.#pending.delete(data.id);
    };
  }

  run(payload) {
    const id = this.#nextId++;
    return new Promise((resolve) => {
      this.#pending.set(id, { resolve });
      this.#worker.postMessage({ id, payload });
    });
  }
}

const pool = new WorkerPool('worker.js');
const risultato = await pool.run([50000, 60000, 70000]);

Così il resto del codice tratta il Worker come una normale funzione asincrona, senza gestire manualmente l’accoppiamento tra richieste e risposte.

In sintesi

Un Web Worker vale il costo di implementazione quando il collo di bottiglia è calcolo JavaScript puro e prolungato — parsing di payload grandi, elaborazione di immagini/audio lato client, algoritmi che richiedono decine o centinaia di millisecondi. Per capire se quel tempo di blocco sta davvero danneggiando l’esperienza utente, vale la pena leggere anche l’articolo sui Core Web Vitals, dove l’INP misura esattamente questo: quanto tempo passa tra un’interazione dell’utente e la risposta visibile dell’interfaccia.

Condividi:XLinkedInFacebookWhatsApp