Trabajadores dedicados, compartidos y de servicio: elegir el hilo del navegador adecuado
Comprenda en qué se diferencian los trabajadores dedicados, compartidos y de servicio en cuanto al alcance, el mensaje y el propósito, y aprenda cuándo cada uno mejora realmente una aplicación web.
El JavaScript de la página se ejecuta en un hilo principal, pero las aplicaciones modernas siguen siendo responsivas al procesar archivos grandes, funcionando sin conexión y recibiendo notificaciones push mientras están inactivas. Gran parte de esto se debe a los workers, que ejecutan scripts fuera del hilo principal. Sin embargo, “worker” abarca tres herramientas diferentes, y elegir la incorrecta desperdicia esfuerzos. Esta guía muestra para qué sirve cada tipo, cómo se comunican y cuándo no vale la pena usarlos.
La familia consta de tres miembros:
Web Workers
│
├── Dedicated Worker
│
├── Shared Worker
│
└── Service Worker
Por qué el hilo principal necesita ayuda
Por defecto, su código comparte un hilo con las actualizaciones del DOM, el manejo de entradas, la renderización y la animación:
JavaScript
↓
DOM updates
↓
User interactions
↓
Rendering
↓
Animations
Dado que estas tareas se realizan por turnos, una llamada síncrona larga como la que se muestra a continuación mantiene ocupado el hilo hasta que devuelve su resultado:
const result = expensiveCalculation();
Hasta que finaliza, el navegador no puede procesar entradas ni dibujar un frame. El usuario experimenta esto:
User clicks button
↓
Heavy JavaScript starts
↓
Main thread is busy
↓
UI becomes sluggish
↓
Calculation finishes
↓
UI becomes responsive again
Un worker resuelve esto ejecutando JavaScript en su propio contexto separado, de modo que el hilo principal permanece libre para tareas relacionadas con la interfaz.
Cómo cooperan una página y un worker
Imagínese dos hilos conectados por un canal de mensajes: el hilo principal se encarga de la interfaz de usuario, el DOM, los eventos y el renderizado, mientras que el worker se ocupa de los cálculos, el análisis y el procesamiento:
Browser
│
┌─────────┴─────────┐
│ │
↓ ↓
Main Thread Worker Thread
│ │
UI / DOM Heavy Work
Events Calculations
Rendering Parsing
Interaction Processing
│ │
└──── Messages ─────┘
La página envía datos a un worker llamando a postMessage en el objeto worker:
worker.postMessage(data);
Dentro del worker, la función global postMessage envía el resultado de vuelta a la página:
postMessage(result);
Las dos partes no comparten variables comunes; todo se transmite como un mensaje.
Por qué los workers no pueden acceder al DOM
El ámbito global de un worker no tiene acceso a estos elementos:
document
window
DOM elements
Una línea como esta dentro de un archivo worker simplemente falla, porque document no está definido allí:
// worker.js
document.querySelector("#app");
El patrón adecuado es realizar el cálculo en el worker, enviar el resultado y dejar que el hilo principal lo aplique a la página:
Main Thread
│
│ postMessage(data)
↓
Worker
│
│ performs calculation
↓
Worker
│
│ postMessage(result)
↓
Main Thread
│
↓
Update DOM
Tener la propiedad del DOM en un solo hilo significa que el código de fondo nunca puede cambiar la interfaz durante el renderizado.
Los tres tipos de workers de un vistazo
Sus ámbitos difieren: un creador, varios contextos del mismo origen o la capa de red:
┌──────────────────────────────┐
│ Web Workers │
├──────────────────────────────┤
│ │
│ Dedicated Worker │
│ → One script/client │
│ │
│ Shared Worker │
│ → Multiple same-origin │
│ browsing contexts │
│ │
│ Service Worker │
│ → Network / caching / │
│ offline / background │
│ capabilities │
│ │
└──────────────────────────────┘
Workers dedicados para cálculos intensivos
Un worker dedicado es propiedad del script que lo crea. Cuando un equipo dice “mueva ese cálculo a un worker”, se refiere a este tipo.
En el lado de la página, se crea el worker desde una URL de script, se le envía un número y se registra lo que devuelve:
const worker = new Worker("worker.js");
worker.postMessage(1000000);
worker.onmessage = (event) => {
console.log("Result:", event.data);
};
El trabajador escucha los mensajes, suma todos los enteros inferiores al valor que recibe y responde con el total:
// worker.js
self.onmessage = (event) => {
const number = event.data;
let result = 0;
for (let i = 0; i < number; i++) {
result += i;
}
self.postMessage(result);
};
El viaje de ida y vuelta es sencillo:
Main Thread
│
│ 1000000
↓
Dedicated Worker
│
│ Calculate
↓
Result
│
↓
Main Thread
El trabajador está vinculado a su creador y nunca se comparte con páginas no relacionadas; dos pestañas tienen dos trabajadores independientes.
Buenos candidatos para un trabajador dedicado
Son útiles para tareas que consumen mucho procesador en una sola aplicación. La transformación de un gran paquete API es típica: el trabajador reorganiza los datos y el hilo principal solo los muestra.
API Response
↓
Large JSON
↓
Worker
↓
Parse / Transform
↓
Main Thread
↓
Render UI
El redimensionado y filtrado de imágenes siguen el mismo patrón:
Image
↓
Worker
↓
Resize / Transform
↓
Result
↓
UI
Lo mismo ocurre con el filtrado, ordenamiento y agregación de grandes conjuntos de datos:
Large Dataset
↓
Worker
↓
Filtering
Sorting
Aggregation
↓
UI
También encajan la cifrado, compresión, análisis de archivos grandes y cálculos intensivos. La regla: si el trabajo que consume mucho procesador hace que la interfaz se ralentice, considere usar un trabajador dedicado.
Trabajadores compartidos para varias pestañas
Supongamos ahora que la misma aplicación está abierta en varios contextos de navegación al mismo tiempo:
Tab A
Tab B
Tab C
Un trabajador compartido permite que los scripts de varias ventanas, pestañas o iframes se conecten a un único trabajador, siempre y cuando compartan el mismo origen:
Shared Worker
/ | \
/ | \
Tab A Tab B Tab C
Sin él, cada pestaña crea su propia copia:
Tab A → Worker A
Tab B → Worker B
Tab C → Worker C
Con él, cada pestaña se conecta a una única instancia:
Tab A ─┐
Tab B ─┼──> Shared Worker
Tab C ─┘
Comunicación a través de puertos
Se accede a un trabajador compartido mediante un MessagePort explícito. La página inicia el puerto y envía un mensaje:
const worker = new SharedWorker("worker.js");
worker.port.start();
worker.port.postMessage("Hello");
Cada nuevo cliente dispara un evento connect en el trabajador, cuyo manejador escucha en el puerto de ese cliente:
self.onconnect = (event) => {
const port = event.ports[0];
port.onmessage = (event) => {
console.log(event.data);
};
};
La puerta de enlace representa la principal diferencia práctica con respecto a un trabajador dedicado: con muchos clientes, cada conexión necesita su propio canal, y las respuestas vuelven a través de la puerta de enlace de la pestaña correspondiente.
Un escenario con múltiples pestañas
Imagínese una herramienta interna donde diferentes pestañas muestran vistas distintas:
Tab 1 → Dashboard
Tab 2 → Reports
Tab 3 → Analytics
Un único componente de fondo podría almacenar el estado común o coordinar la comunicación entre todas ellas:
Shared Worker
│
┌──────────┼──────────┐
↓ ↓ ↓
Tab 1 Tab 2 Tab 3
│ │ │
└──────────┼──────────┘
↓
Shared State
Esto evita que cada pestaña necesite su propia instancia de trabajador, pero todos los contextos deben compartir un origen. El soporte para trabajadores compartidos también ha estado rezagado históricamente con respecto a los trabajadores dedicados, especialmente en algunos navegadores móviles; por lo tanto, verifique primero los datos de compatibilidad actuales.
Trabajadores de servicio para el comportamiento en red y sin conexión
El trabajador de servicio es el tipo más malentendido. No se trata de un trabajador dedicado con otro nombre; se sitúa entre su aplicación, el navegador y la red:
Browser
│
↓
Service Worker
│
├── Cache
│
├── Network
│
├── Offline response
│
└── Background capabilities
Dado que intercepta las solicitudes, sienta las bases para experiencias sin conexión, caché, manejo personalizado de solicitudes, notificaciones push y sincronización en segundo plano.
Situado entre la página y el servidor
Imaginemos que un usuario visita su sitio:
https://example.com
Si no hay un service worker, cada solicitud viaja directamente desde el navegador al servidor:
Browser
↓
Internet
↓
Server
Con uno registrado, las solicitudes pasan primero por él, y este puede servirlas desde el caché o reenviarlas a la red:
Browser
↓
Service Worker
↓
├── Cache
│
└── Network
Él decide el destino de cada solicitud. Una estrategia que prioriza el caché devuelve resultados inmediatamente y, de lo contrario, los obtiene y los almacena en caché cuando es apropiado:
Request
↓
Is it cached?
│
├── YES → Return cached response
│
└── NO
↓
Network
↓
Response
↓
Cache if appropriate
Las aplicaciones compatibles con modo sin conexión se construyen sobre esta lógica. Para ver un ejemplo paso a paso, consulte habilitar el soporte offline en aplicaciones web con service workers.
Registro y ciclo de vida
El navegador gestiona un ciclo de vida, simplificado aquí:
Register
↓
Download
↓
Install
↓
Activate
↓
Control Pages
Se comienza registrando un script cuando la API existe:
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js");
}
Luego el navegador se encarga de la instalación, activación y actualizaciones. En contraste, un worker dedicado proviene de una llamada directa al constructor, lo cual nunca ocurre con los service workers:
new Worker(...)
Estos requieren un contexto seguro (HTTPS, con localhost permitido en desarrollo). Un nuevo worker no controla las páginas ya abiertas hasta que se recarga, a menos que las reclame, lo cual a menudo confunde las pruebas.
Interceptación de solicitudes
Un service worker mínimo escucha los eventos fetch; este solo registra cada URL:
self.addEventListener("fetch", (event) => {
console.log("Request:", event.request.url);
});
El caché real se basa en ese mecanismo. Para app.js: servir desde el caché si está disponible, de lo contrario buscarlo, almacenarlo y devolverlo:
User requests app.js
↓
Service Worker
↓
Is app.js cached?
/ \
YES NO
↓ ↓
Return Network
Cache ↓
Cache
↓
Return
De allí su papel central en las Aplicaciones Web Progresivas (PWAs) y los diseños orientados a funcionar sin conexión.
Comparación de los tres uno al lado del otro
Trabajador dedicado
La versión breve es “un hilo de fondo que pertenece a una página”:
Page
│
↓
Dedicated Worker
Es adecuado para cálculos que consumen muchos recursos de la CPU, análisis de datos, procesamiento de imágenes y tratamiento de información.
Trabajador compartido
La versión breve es “un trabajador que varias páginas del mismo origen pueden utilizar”:
Tab A ─┐
Tab B ─┼──> Shared Worker
Tab C ─┘
Es adecuado para tareas en segundo plano compartidas entre diferentes contextos de navegación, así como para la comunicación o el estado compartido.
Trabajador de servicio
La versión breve es “una capa entre la aplicación y la red”:
App
↓
Service Worker
↓
Cache / Network
Es adecuado para aplicaciones sin conexión, almacenamiento en caché, intercepción de solicitudes, notificaciones push y sincronización en segundo plano.
Resumen en una línea de cada uno
En una sola línea para cada uno:
Dedicated Worker
↓
"Do this heavy computation for me."
Shared Worker
↓
"Let multiple pages use this worker."
Service Worker
↓
"Help my web application interact with
the network and browser capabilities."
De esa manera, los tres son difíciles de confundir.
Ambito, comunicación y uso típico
Dedicado: un script, cálculo en segundo plano, postMessage(), sin DOM:
Scope:
One script
Main purpose:
Background computation
Communication:
postMessage()
DOM access:
❌ No
Common use:
Heavy computation
Compartido: varios contextos del mismo origen, MessagePort, sin DOM, coordinación entre pestañas:
Scope:
Multiple same-origin contexts
Main purpose:
Shared background work
Communication:
MessagePort
DOM access:
❌ No
Common use:
Cross-tab/shared worker communication
Service: páginas dentro de su origen y ámbito de ruta, eventos y APIs de la plataforma, sin DOM, caché, modo offline, envío y sincronización:
Scope:
Origin/path controlled pages
Main purpose:
Network + background capabilities
Communication:
Events / messaging / APIs
DOM access:
❌ No
Common use:
Caching, offline, push, background sync
Cuando un worker ralentiza las cosas
Los workers no son automáticamente más rápidos. Su inicio implica costos, y los datos deben trasladarse entre contextos:
Main Thread
↕
Worker
Esa comunicación también lleva tiempo. Para una tarea pequeña, la sobrecarga puede superar al propio trabajo:
Small Task
↓
Worker overhead
↓
Communication
↓
Actual calculation
Entonces, el worker puede ser más lento que el código en línea. Úselo solo cuando la tarea sea lo suficientemente pesada como para que su transferencia genere una mejora visible.
Qué es lo que realmente cruza el límite
Los mensajes se copian mediante el algoritmo de clonación estructurada, por lo que las cargas grandes incrementan el tiempo de copiado. Los objetos transferibles como ArrayBuffer se mueven sin copiarse, pero el remitente pierde acceso a ellos. SharedArrayBuffer permite un verdadero uso compartido de memoria solo bajo requisitos de seguridad adicionales (aislamiento entre orígenes). Para la mayoría de las aplicaciones, prefiera los mensajes explícitos:
Main Thread
↓
postMessage()
↓
Worker
↓
postMessage()
↓
Main Thread
Uso de un worker dedicado en React
Si una aplicación React procesa un conjunto de datos grande durante la renderización o en un manejador, la interfaz de usuario se detiene:
React UI
↓
Large calculation
↓
Main thread blocked
↓
UI becomes sluggish
Con un worker dedicado, el componente envía los datos y actualiza el estado cuando llega el resultado:
React UI
│
├──────────────> Worker
│ │
│ ↓
│ Calculation
│ │
│ ↓
│<──────────── Result
│
↓
Update UI
React sigue renderizando mientras el worker realiza los cálculos. Crea el worker en un efecto y llama a terminate() en su función de limpieza para que no sobreviva al componente. Esto es adecuado para la visualización de datos, editores de imágenes, procesamiento de audio y video, archivos grandes y análisis en el lado del cliente.
Elegir el worker adecuado
Los scripts en JavaScript que consumen muchos recursos de la CPU requieren un worker dedicado:
Do you have CPU-heavy JavaScript?
│
YES
↓
Use Dedicated Worker
Si el requisito es este:
Multiple tabs/pages need
the same worker
entonces el tipo que se debe evaluar es:
Shared Worker
Y si el requisito involucra alguno de estos elementos:
Caching
Offline support
Network interception
Push notifications
Background sync
entonces la respuesta es:
Service Worker
Errores comunes
Mover todo a un worker
Utiliza workers solo cuando resuelvan un problema medible.
Esperar acceso al DOM
Esto no funcionará:
worker.document.querySelector(...);
Los workers envían datos de vuelta; el hilo principal actualiza el DOM.
Uso de un service worker para cálculos
Los service workers se enfocan en la red y en el ciclo de vida de la aplicación, y el navegador puede detenerlos cuando están inactivos. Para cálculos puramente numéricos como:
1 million records
↓
complex calculation
Comience con un worker dedicado.
Ignorar los costos de las comunicaciones
Cada intercambio implica un costo adicional:
Main Thread
↕
Messaging
↕
Worker
Mueva solo la cantidad de trabajo suficiente para abarcarlo.
Conclusión
El principio rector: el JavaScript costoso no debe bloquear el hilo principal sin motivo. Los tres tipos se corresponden con tres problemas:
Web Workers
│
┌──────────────┼──────────────┐
↓ ↓ ↓
Dedicated Shared Service
Worker Worker Worker
│ │ │
One script Multiple Network /
uses it contexts offline
- Worker dedicado: cálculos intensivos para una sola página.
- Worker compartido: trabajo en segundo plano compartido por varios contextos del mismo origen.
Antes de adoptar uno, mida la tarea que genera bloqueos, confirme que supera los costos adicionales de las comunicaciones y verifique el soporte del navegador. Visto de esta manera, los service workers son tres soluciones específicas a tres preguntas distintas.
Lecturas relacionadas
- Habilitar soporte offline en aplicaciones web con service workers — Aprenda cómo utilizar Service Workers y la API Cache para que un sitio web se cargue al instante y siga funcionando incluso sin conexión a Internet.
- Cómo pinta el navegador y dónde encaja React — Aprenda cómo el Critical Rendering Path, la reconciliación, Fiber y el Scheduler trabajan juntos para convertir las actualizaciones de React en píxeles en la pantalla.