Inicio / Artículos / Trabajadores dedicados, compartidos y de servicio: elegir el hilo del navegador adecuado

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.

2425 palabras

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.
  • Service worker: intercepción de red, almacenamiento en caché y funcionalidades sin conexión.
  • 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