W3docs

Web Workers de JavaScript

Aprende JavaScript Web Workers para ejecutar código en un hilo secundario — mantén la UI responsiva durante tareas pesadas, comunícate con postMessage y transfiere datos eficientemente con objetos transferibles.

JavaScript ejecuta tu código en un único hilo principal — el mismo hilo que construye el diseño de la página, pinta los píxeles y gestiona los clics y las pulsaciones de teclado. Ese único hilo es el corazón del event loop: toma una tarea, la ejecuta hasta completarla y luego pasa a la siguiente. Por eso, cuando una función realiza algo realmente pesado — procesar un array grande, analizar un archivo de varios megabytes, cifrar una contraseña miles de veces — el bucle queda atrapado dentro de esa función. Nada más puede ocurrir: el desplazamiento se entrecorta, los botones dejan de responder y la página parece congelada hasta que el trabajo termina.

Web Workers resuelven esto ejecutando un script en un hilo secundario separado, en paralelo con el hilo principal. El trabajo pesado se aparta del camino crítico, la UI se mantiene responsiva y los dos hilos se comunican pasándose mensajes.

Por Qué los Temporizadores No Son Suficientes

El primer instinto habitual es envolver el trabajo lento en setTimeout con la esperanza de que se ejecute "en segundo plano". No lo hace. Los temporizadores solo difieren una tarea a un turno posterior del mismo bucle — cuando el callback finalmente se dispara, sigue ejecutándose en el hilo principal y sigue bloqueando todo mientras se ejecuta. (Consulta scheduling con setTimeout y setInterval para entender cómo funciona realmente esa cola.)

// 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 es diferente: su código se ejecuta en un hilo completamente distinto, por lo que el hilo principal queda libre para seguir renderizando y respondiendo mientras el worker trabaja.

Crear un Worker

Un worker es un archivo JavaScript separado. Se crea apuntando el constructor Worker a la URL de ese archivo:

const worker = new Worker('worker.js');

El navegador lanza un nuevo hilo, descarga worker.js y empieza a ejecutarlo. A partir de ese momento, los dos scripts se comunican únicamente a través de mensajes — no comparten variables, funciones ni objetos.

Aquí está el par de archivos más pequeño y completo.

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');
};

Comunicación con postMessage

La comunicación es bidireccional y asíncrona. El hilo principal llama a worker.postMessage(data) y escucha con worker.onmessage; dentro del worker, self.postMessage(data) envía la respuesta y self.onmessage recibe. Cada manejador recibe un MessageEvent, y el payload vive en su propiedad .data.

Los datos que se envían son copiados, no compartidos, usando el algoritmo de clonación estructurada. Esto significa que puedes pasar strings, números, booleanos, arrays, objetos planos, Map, Set, Date, ArrayBuffer y más — pero no funciones, nodos del DOM ni instancias de clase con métodos. Al ser una copia, mutar un objeto en un lado nunca afecta al otro.

Aquí hay un viaje de ida y vuelta completo. El worker calcula el número de Fibonacci número n con el algoritmo recursivo ingenuo — deliberadamente lento, exactamente el tipo de trabajo que congela la UI si se ejecuta en el hilo principal.

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 });
};

El hilo principal lanza la solicitud y vuelve inmediatamente a gestionar clics y renderizado. Cuando el worker termina, su resultado llega como un mensaje — sin congelamiento, sin tirones en el indicador de carga.

El Ámbito Global del Worker

Dentro de un worker no existe window. El objeto global es self (un DedicatedWorkerGlobalScope), y lo más importante es que no hay document ni DOM. Un worker no puede leer ni modificar la página; si necesita actualizar la UI, envía un mensaje y deja que el hilo principal lo haga.

Advertencia

El código dentro de un Web Worker no puede tocar el DOM. No existe document, no existe window y no hay acceso a los elementos de la página. Cualquier elemento visual debe devolverse al hilo principal mediante postMessage. Esta restricción es lo que hace que los workers sean seguros para ejecutarse en paralelo — no hay estado de UI compartido que pueda corromperse.

Sin embargo, los workers están lejos de estar vacíos. El ámbito del worker te proporciona muchas APIs útiles:

  • importScripts('a.js', 'b.js') para cargar scripts clásicos de forma síncrona.
  • fetch y XMLHttpRequest para solicitudes de red.
  • Temporizadores: setTimeout, setInterval.
  • console, crypto, TextEncoder / TextDecoder, WebSocket, IndexedDB y muchos más.

Esto convierte a los workers en un lugar ideal para las solicitudes de red, el análisis, la compresión y el cifrado — trabajo autocontenido que produce un resultado que puedes enviar de vuelta a la página.

Gestión de Errores y Terminación

Si un worker lanza un error no capturado, este aparece en el hilo principal a través de worker.onerror:

worker.onerror = (event) => {
  console.error(`Worker error: ${event.message} (${event.filename}:${event.lineno})`);
};

Un worker sigue ejecutándose hasta que se detiene. Puedes detenerlo desde el exterior con worker.terminate(), que mata el hilo de inmediato — cualquier trabajo en curso se abandona:

worker.terminate();

O el worker puede cerrarse por sí mismo desde dentro una vez que termina:

// inside worker.js
self.close();

Terminar workers inactivos libera memoria; los workers de larga duración que manejan muchos mensajes pueden mantenerse activos sin problema.

Objetos Transferibles: Mover en Lugar de Copiar

La clonación estructurada es conveniente, pero copia los datos. Para un payload binario grande — digamos un buffer de imagen de 50 MB — copiar ambos desperdicia memoria y consume tiempo. Los objetos transferibles permiten transferir la propiedad en su lugar: los datos se mueven al otro hilo sin ninguna copia, y el remitente pierde el acceso a ellos.

Para activarlo, se pasa un segundo argumento a postMessage — una lista de los objetos a transferir:

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 it

Tras la transferencia, buffer.byteLength es 0 en el lado del remitente: la memoria ahora pertenece al worker. Transferir es ideal para ArrayBuffers y los typed arrays construidos sobre ellos — consulta ArrayBuffer y arrays binarios para ver cómo se estructura ese dato binario. Otros transferibles incluyen MessagePort, ImageBitmap y OffscreenCanvas.

Nota

El contenido del segundo argumento también debe aparecer en el mensaje. En worker.postMessage({ buffer }, [buffer]), el buffer está referenciado por el payload y listado como transferible. Si listas algo que no es accesible desde el mensaje, el navegador lanza un DataCloneError.

Workers de Módulo

Por defecto un worker es un script clásico, por lo que usa importScripts() en lugar de la sintaxis de módulos ES. Pasa { type: 'module' } y el worker se convierte en un worker de módulo que puede usar import estático y dinámico:

const worker = new Worker('worker.js', { type: 'module' });

worker.js

import { compress } from './compression.js';

self.onmessage = (event) => {
  self.postMessage(compress(event.data));
};

Los workers de módulo son la opción moderna predeterminada para código nuevo — te ofrecen importaciones correctas, modo estricto y un grafo de dependencias más limpio.

Otros Tipos de Workers

Un simple new Worker(...) crea un worker dedicado: pertenece a la única página que lo creó. Existen dos tipos de workers relacionados — pero distintos — que deberías poder diferenciar:

  • SharedWorker — una única instancia de worker compartida entre múltiples pestañas, ventanas o iframes del mismo origen. Las páginas se conectan a él a través de un MessagePort, lo que lo hace útil para coordinar estado o una única conexión de red entre pestañas. No es un worker dedicado más rápido; es uno compartido.
  • Service Worker — un worker especial que actúa como proxy de red, situándose entre la página y la red para habilitar caché, soporte sin conexión y notificaciones push. Es orientado a eventos y persiste más allá de la vida útil de la página. Eso es un trabajo diferente al de un worker dedicado de "ejecutar este cómputo fuera del hilo"; para esa parte, lee Service Workers.
Información

Regla general: usa un Web Worker dedicado para mover trabajo intensivo en CPU fuera del hilo principal, un SharedWorker para compartir un worker entre pestañas del mismo sitio, y un Service Worker para controlar solicitudes de red y construir aplicaciones con soporte sin conexión.

Cuándo Usar Web Workers

Los Web Workers valen la pena siempre que una tarea sea intensiva en CPU y lo suficientemente larga como para percibirse como un problema de rendimiento:

  • Cómputo pesado — física, análisis de datos, ordenaciones y agregaciones de grandes volúmenes.
  • Procesamiento de imágenes y vídeo, incluyendo manipulación de píxeles fuera de pantalla.
  • Análisis y compresión de archivos grandes (CSV, JSON, archivos comprimidos).
  • Criptografía — cifrado y hashing sin congelar la entrada.
  • Procesamiento de grandes conjuntos de datos antes de devolver un resultado compacto al renderizador.

Si el cuello de botella es esperar a la red en lugar de computar, generalmente no necesitas un worker — fetch ya es asíncrono y no bloqueante. Los workers destacan cuando la CPU en sí es lo que mantiene ocupado al hilo principal.

Pon a Prueba Tu Conocimiento

Práctica
¿Puede el código que se ejecuta dentro de un Web Worker acceder al DOM directamente?
¿Puede el código que se ejecuta dentro de un Web Worker acceder al DOM directamente?
Práctica
¿Cómo llegan al worker los datos pasados a worker.postMessage(data) por defecto?
¿Cómo llegan al worker los datos pasados a worker.postMessage(data) por defecto?
Práctica
¿Cuál es la principal ventaja de transferir un ArrayBuffer con worker.postMessage(buffer, [buffer])?
¿Cuál es la principal ventaja de transferir un ArrayBuffer con worker.postMessage(buffer, [buffer])?
Was this page helpful?