JavaScript es monohilo: todo el código de tu aplicación comparte el mismo hilo principal con el renderizado del navegador. Cuando ese hilo se bloquea durante más de 50ms (el umbral de Long Task según Google), la interfaz se congela y el usuario lo nota. Los Web Workers permiten mover trabajo intensivo a hilos separados sin tocar el hilo principal.
Crear un Worker y comunicarse con postMessage
Un Worker se crea apuntando a un fichero JavaScript separado. La comunicación entre el hilo principal y el Worker se hace exclusivamente mediante mensajes, sin memoria compartida por defecto.
// main.js
const worker = new Worker('./worker.js');
worker.postMessage({ tipo: 'calcular', datos: [1, 2, 3, 4, 5] });
worker.onmessage = (e) => {
console.log('Resultado del worker:', e.data);
};
worker.onerror = (err) => {
console.error('Error en worker:', err.message);
};
// worker.js
self.onmessage = (e) => {
if (e.data.tipo === 'calcular') {
const resultado = e.data.datos.reduce((acc, n) => acc + n * n, 0);
self.postMessage(resultado);
}
};
Inline Workers con Blob URL
Para evitar tener que crear un fichero separado puedes crear un Worker a partir de un Blob, especialmente útil en aplicaciones de una sola página:
const codigo = `
self.onmessage = ({ data }) => {
// Cálculo CPU-intensivo en el worker
let resultado = 0;
for (let i = 0; i < data; i++) resultado += Math.sqrt(i);
self.postMessage(resultado);
};
`;
const blob = new Blob([codigo], { type: 'application/javascript' });
const url = URL.createObjectURL(blob);
const worker = new Worker(url);
worker.postMessage(10_000_000);
worker.onmessage = (e) => {
URL.revokeObjectURL(url); // liberar memoria
console.log('Resultado:', e.data);
};
Transferir datos grandes con Transferable
Por defecto, postMessage copia los datos mediante el algoritmo de clonación estructurada. Para ArrayBuffer, MessagePort y OffscreenCanvas puedes transferirlos en lugar de copiarlos: el objeto se mueve al Worker y el hilo principal pierde acceso a él, evitando la copia.
// main.js
const buffer = new ArrayBuffer(1024 * 1024); // 1 MB
// Transferir (no copiar): el buffer se mueve al worker
worker.postMessage({ buffer }, [buffer]);
// Aquí buffer.byteLength === 0, ya no es accesible
// worker.js
self.onmessage = ({ data: { buffer } }) => {
const vista = new Uint8Array(buffer);
// Procesar el buffer...
self.postMessage({ resultado: vista.length }, [buffer]); // devolver el buffer
};
SharedArrayBuffer y Atomics
SharedArrayBuffer permite que el hilo principal y los Workers lean y escriban en el mismo bloque de memoria. Requiere que el servidor envíe los headers Cross-Origin-Opener-Policy: same-origin y Cross-Origin-Embedder-Policy: require-corp.
// main.js
const shared = new SharedArrayBuffer(4); // 4 bytes = 1 Int32
const vista = new Int32Array(shared);
Atomics.store(vista, 0, 0); // inicializar a 0
worker.postMessage({ shared });
// Esperar a que el worker señale que terminó
Atomics.wait(vista, 0, 0); // bloquea (solo en Worker, no en main thread)
console.log('Worker terminó, valor:', Atomics.load(vista, 0));
// worker.js
self.onmessage = ({ data: { shared } }) => {
const vista = new Int32Array(shared);
// Trabajo intensivo...
Atomics.store(vista, 0, 1); // marcar como listo
Atomics.notify(vista, 0, 1); // despertar al que espera
};
Usa Atomics.add, Atomics.compareExchange y demás métodos para operaciones atómicas seguras en el buffer compartido, evitando condiciones de carrera al actualizar contadores o flags desde múltiples hilos.
Pool de Workers para trabajo paralelo
Crear un Worker por tarea es costoso. Un pool reutiliza un número fijo de Workers y asigna tareas a los que estén libres:
class WorkerPool {
#workers = [];
#cola = [];
constructor(url, tamaño = navigator.hardwareConcurrency) {
for (let i = 0; i < tamaño; i++) {
const w = new Worker(url);
w._libre = true;
w.onmessage = (e) => this.#completar(w, e.data);
this.#workers.push(w);
}
}
ejecutar(datos) {
return new Promise((resolve) => {
const libre = this.#workers.find(w => w._libre);
if (libre) {
libre._libre = false;
libre._resolver = resolve;
libre.postMessage(datos);
} else {
this.#cola.push({ datos, resolve });
}
});
}
#completar(worker, resultado) {
worker._resolver(resultado);
const siguiente = this.#cola.shift();
if (siguiente) {
worker._resolver = siguiente.resolve;
worker.postMessage(siguiente.datos);
} else {
worker._libre = true;
}
}
}
