Se sviluppi con JavaScript, è probabile che tu abbia riscontrato il problema delle interfacce bloccate durante operazioni impegnative: come la lettura di file JSON di grandi dimensioni o l’elaborazione di immagini. Questo accade perché JavaScript opera su un modello a…
Se sviluppi con JavaScript, è probabile che tu abbia riscontrato il problema delle interfacce bloccate durante operazioni impegnative: come la lettura di file JSON di grandi dimensioni o l’elaborazione di immagini. Questo accade perché JavaScript opera su un modello a thread singolo, dove ogni attività, dalla gestione degli eventi al rendering della pagina, avviene in un unico thread, il cosiddetto main thread. Se questo thread è occupato, l’utente non può interagire con l’interfaccia.
Nella guida di oggi, esploreremo possibili soluzioni a questa limitazione, focalizzandoci sull’utilizzo dei Web Workers, che consentono il vero parallelismo nel browser.
Cos’è il modello a thread singolo di JavaScript e quale problema crea
Per capire l’importanza dei Web Workers, è fondamentale comprendere il funzionamento di JavaScript nel browser. Ogni scheda del browser opera attraverso un processo che contiene un solo thread JavaScript, il quale gestisce la logica dell’applicazione, compreso il parsing dell’HTML, l’esecuzione degli script e le animazioni CSS. Il motore di JavaScript (come V8, SpiderMonkey o JavaScriptCore) elabora le istruzioni una dopo l’altra.
L’event loop è ciò che rende questa architettura sostenibile: quando un’operazione asincrona come una chiamata a fetch o setTimeout è completata, il suo callback viene messo in coda e attende che il thread si liberi. Tuttavia, se il thread è occupato da un’operazione sincrona che dura anche solo 200 millisecondi, nessun evento del mouse sarà elaborato e l’interfaccia apparirà congelata.
I Web Workers: come creare un secondo thread in JavaScript
I Web Workers rappresentano un thread separato che il browser offre agli sviluppatori. Anche se non possono interagire direttamente con il DOM o utilizzare l’oggetto window, possono eseguire codice JavaScript in parallelo rispetto al main thread. La comunicazione tra il main thread e il worker avviene attraverso un sistema a messaggi, utilizzando postMessage per inviare dati e onmessage per riceverli.
Quando i dati vengono scambiati, vengono serializzati e deserializzati, quindi ogni messaggio è una copia indipendente, non un riferimento condiviso. Creare un worker è semplice; basta creare un file JavaScript separato per il codice da eseguire in background e istanziare un oggetto Worker nel main thread puntando a quel file.
// main.js
const worker = new Worker('worker.js');
worker.postMessage({ tipo: 'calcola', dati: arrayDiNumeri });
worker.onmessage = function(evento) {
console.log('Risultato dal worker:', evento.data);
};
worker.onerror = function(errore) {
console.error('Errore nel worker:', errore.message);
};
// worker.js
self.onmessage = function(evento) {
const { tipo, dati } = evento.data;
if (tipo === 'calcola') {
const risultato = dati.reduce((acc, n) => acc + n, 0);
self.postMessage({ risultato });
}
};
Nell’esempio precedente, il main thread invia un array di numeri al worker, il quale calcola la somma e restituisce il risultato. Durante questo processo, il main thread resta reattivo e può gestire eventi e aggiornare l’interfaccia.
Ottimizzare il trasferimento di dati con Object Transferable
La serializzazione dei dati attraverso il metodo di cloning ha un costo, specialmente con dataset di grosse dimensioni. Per migliorare l’efficienza, JavaScript offre gli Object Transferable: invece di copiare un ArrayBuffer, il buffer viene trasferito dal thread mittente a quello ricevente. In questo modo, il thread originale non ha più accesso a quel buffer dopo il trasferimento, un meccanismo simile a quello di linguaggi come Rust o C++.
// Creazione di un buffer
const buffer = new ArrayBuffer(1024 * 1024 * 10); // 10 MB
worker.postMessage({ buffer }, [buffer]);
// Dopo questa riga, il buffer è vuoto nel main thread
// La proprietà è stata trasferita al worker
Questo approccio è estremamente efficace per gestire immagini, audio e altre strutture dati pesanti.
Conclusione
In questo articolo abbiamo esaminato come affrontare le limitazioni del modello a thread singolo di JavaScript, con un’attenzione particolare ai Web Workers. Utilizzarli può notevolmente migliorare l’esperienza utente, consentendo un’interfaccia reattiva anche durante operazioni intensive. Nella prossima guida, esploreremo ulteriormente gli SharedArrayBuffer e come gestire la memoria condivisa tra thread, per sfruttare al massimo le potenzialità di JavaScript.
