JavaScript corre en un solo hilo. Siempre fue así, y tiene sentido: simplifica el modelo de concurrencia, elimina los race conditions más comunes, y hace que escribir código asincrónico con async/await sea razonablemente predecible.
El problema aparece cuando una operación es CPU-intensiva. Procesar una imagen grande, parsear un archivo pesado, ejecutar un algoritmo de encriptación — cualquier tarea que tarde más de unos pocos milisegundos bloquea ese único hilo. En el browser, la UI se congela. En Node.js, el servidor deja de responder requests mientras termina.
Los Workers son la solución: hilos separados de ejecución que corren en paralelo sin bloquear el hilo principal. Pero hay dos tipos — y elegir el incorrecto no funciona.
Web Workers — concurrencia en el browser
Un Web Worker corre en un hilo separado dentro del browser. No tiene acceso al DOM, no puede tocar window, y se comunica con el hilo principal únicamente por mensajes.
El patrón básico tiene dos archivos: el hilo principal que crea el worker y le manda trabajo, y el archivo del worker que recibe ese trabajo y devuelve el resultado.
// main.js — hilo principal
const worker = new Worker('worker.js')
worker.postMessage({ data: arrayGrande })
worker.onmessage = (event) => {
console.log('Resultado:', event.data.result)
}
// worker.js — hilo separado
self.onmessage = (event) => {
const resultado = procesarDatos(event.data.data) // operación pesada
self.postMessage({ result: resultado })
}
El hilo principal no se bloquea mientras el worker procesa. La UI sigue respondiendo.
Limitaciones importantes:
- Sin acceso al DOM ni a APIs como
localStorage— el worker vive en su propio contexto aislado - La comunicación es asincrónica y por copia — los objetos que enviás con
postMessagese serializan, no se comparten por referencia (exceptoSharedArrayBufferyArrayBuffertransferibles) - Solo funciona en browsers con same-origin — no podés cargar un worker desde otro dominio
Cuándo usarlo: procesamiento de imágenes o audio en el browser, cálculos pesados que el usuario dispara (simulaciones, filtros, compresión), parseo de archivos grandes sin congelar la UI.
Worker Threads — concurrencia en Node.js
El equivalente en Node.js es worker_threads, disponible desde Node 12. El modelo es similar — hilos separados que se comunican por mensajes — pero la API usa parentPort en lugar de self.
// server.js — hilo principal
const { Worker } = require('worker_threads')
const worker = new Worker('./worker.js', {
workerData: { input: datosHeavy }
})
worker.on('message', (result) => {
console.log('Resultado del worker:', result)
})
// worker.js — hilo separado
const { workerData, parentPort } = require('worker_threads')
const resultado = operacionPesada(workerData.input)
parentPort.postMessage(resultado)
A diferencia de child_process.fork(), los Worker Threads comparten memoria con el proceso padre — pueden usar SharedArrayBuffer para transferir datos sin copiarlos. Eso los hace más eficientes para tareas que manejan buffers grandes.
Cuándo usarlo: encriptación o hashing en el servidor, procesamiento de datos en batch, generación de PDFs o imágenes server-side, cualquier tarea CPU-bound que correría en el mismo proceso y bloquearía el event loop de Node.
La diferencia que importa
| Web Workers | Worker Threads | |
|---|---|---|
| Entorno | Browser | Node.js |
| Comunicación | postMessage / onmessage | parentPort.postMessage / .on('message') |
| Acceso al DOM | No | No aplica |
| Memoria compartida | Solo con SharedArrayBuffer | SharedArrayBuffer nativo |
| Overhead | Medio | Bajo |
La confusión más común es intentar usar Web Workers en Node.js o worker_threads en el browser. No son intercambiables — son mecanismos distintos para el mismo problema en entornos distintos.
Lo que no resuelven
Los Workers no son la respuesta a todo. Para operaciones I/O — leer un archivo, hacer un request HTTP, consultar una base de datos — el event loop asincrónico de JavaScript ya maneja la concurrencia bien. Un await fetch(...) no bloquea el hilo; el resultado llega cuando está listo mientras el hilo sigue haciendo otras cosas.
Los Workers son para CPU. Si lo que tenés es una operación que consume tiempo de procesador — no tiempo de espera de red o disco — ahí es donde el modelo de un solo hilo se rompe y los Workers son la solución correcta.
Buenas prácticas
Mantené los workers stateless. Un worker que recibe inputs, hace un cálculo, y devuelve un resultado es predecible y fácil de razonar. Un worker con estado interno que se acumula entre mensajes es una fuente de bugs difíciles.
Terminá los workers cuando ya no los necesitás. Un worker que queda vivo consume recursos. Llamá a worker.terminate() cuando terminaste de usarlo.
Usá transferibles para datos grandes. En vez de copiar un ArrayBuffer con postMessage, transferilo — el buffer pasa de un contexto al otro sin copia. Más rápido, menos memoria.
const buffer = new ArrayBuffer(1024 * 1024 * 10) // 10MB
worker.postMessage(buffer, [buffer]) // segundo arg = lista de transferibles
// buffer ya no es accesible en el hilo principal
El modelo de un solo hilo de JavaScript no es una limitación a superar — es una decisión de diseño con trade-offs claros. Los Workers no reemplazan ese modelo; lo extienden para los casos donde no alcanza.