Worker Compartido en JavaScript

Worker Compartido en JavaScript

Este artículo explica el Shared Worker en JavaScript.

Explicaremos todo, desde los conceptos básicos sobre Shared Worker hasta casos de uso prácticos, paso a paso.

YouTube Video

javascript-shared-worker.html
  1<!DOCTYPE html>
  2<html lang="en">
  3<head>
  4  <meta charset="UTF-8">
  5  <title>JavaScript &amp; HTML</title>
  6  <style>
  7    * {
  8        box-sizing: border-box;
  9    }
 10
 11    body {
 12        margin: 0;
 13        padding: 1em;
 14        padding-bottom: 10em;
 15        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
 16        background-color: #f7f9fc;
 17        color: #333;
 18        line-height: 1.6;
 19    }
 20
 21    .container {
 22        max-width: 800px;
 23        margin: 0 auto;
 24        padding: 1em;
 25        background-color: #ffffff;
 26        border: 1px solid #ccc;
 27        border-radius: 10px;
 28        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 29    }
 30
 31    .container-flex {
 32        display: flex;
 33        flex-wrap: wrap;
 34        gap: 2em;
 35        max-width: 1000px;
 36        margin: 0 auto;
 37        padding: 1em;
 38        background-color: #ffffff;
 39        border: 1px solid #ccc;
 40        border-radius: 10px;
 41        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 42    }
 43
 44    .left-column, .right-column {
 45        flex: 1 1 200px;
 46        min-width: 200px;
 47    }
 48
 49    h1, h2 {
 50        font-size: 1.2rem;
 51        color: #007bff;
 52        margin-top: 0.5em;
 53        margin-bottom: 0.5em;
 54        border-left: 5px solid #007bff;
 55        padding-left: 0.6em;
 56        background-color: #e9f2ff;
 57    }
 58
 59    button {
 60        display: block;
 61        margin: 1em auto;
 62        padding: 0.75em 1.5em;
 63        font-size: 1rem;
 64        background-color: #007bff;
 65        color: white;
 66        border: none;
 67        border-radius: 6px;
 68        cursor: pointer;
 69        transition: background-color 0.3s ease;
 70    }
 71
 72    button:hover {
 73        background-color: #0056b3;
 74    }
 75
 76    #output {
 77        margin-top: 1em;
 78        background-color: #1e1e1e;
 79        color: #0f0;
 80        padding: 1em;
 81        border-radius: 8px;
 82        min-height: 200px;
 83        font-family: Consolas, monospace;
 84        font-size: 0.95rem;
 85        overflow-y: auto;
 86        white-space: pre-wrap;
 87    }
 88
 89    .highlight {
 90        outline: 3px solid #ffc107; /* yellow border */
 91        background-color: #fff8e1;  /* soft yellow background */
 92        transition: background-color 0.3s ease, outline 0.3s ease;
 93    }
 94
 95    .active {
 96        background-color: #28a745; /* green background */
 97        color: #fff;
 98        box-shadow: 0 0 10px rgba(40, 167, 69, 0.5);
 99        transition: background-color 0.3s ease, box-shadow 0.3s ease;
100    }
101  </style>
102</head>
103<body>
104    <div class="container">
105        <h1>JavaScript Console</h1>
106        <button id="executeBtn">Execute</button>
107        <div id="output"></div>
108    </div>
109
110    <script>
111        // Override console.log to display messages in the #output element
112        (function () {
113            // Override console.log
114            const originalLog = console.log;
115            console.log = function (...args) {
116                originalLog.apply(console, args);
117                const message = document.createElement('div');
118                message.textContent = args.map(String).join(' ');
119                output.appendChild(message);
120            };
121
122            // Override console.error
123            const originalError = console.error;
124            console.error = function (...args) {
125                originalError.apply(console, args);
126                const message = document.createElement('div');
127                message.textContent = args.map(String).join(' ');
128                message.style.color = 'red'; // Color error messages red
129                output.appendChild(message);
130            };
131        })();
132
133        document.getElementById('executeBtn').addEventListener('click', () => {
134            // Prevent multiple loads
135            if (document.getElementById('externalScript')) return;
136
137            const script = document.createElement('script');
138            script.src = 'javascript-shared-worker.js';
139            script.id = 'externalScript';
140            //script.onload = () => console.log('javascript-shared-worker.js loaded and executed.');
141            //script.onerror = () => console.log('Failed to load javascript-shared-worker.js.');
142            document.body.appendChild(script);
143        });
144    </script>
145</body>
146</html>

Worker Compartido en JavaScript

¿Qué es un Shared Worker?

Un Shared Worker es un hilo de trabajo que puede ser compartido entre múltiples páginas (pestañas, ventanas, iframes, etc.) dentro del mismo origen. A diferencia de un Dedicated Worker específico de una página, la característica principal es que un solo proceso en segundo plano puede ser compartido entre varias páginas. Los casos de uso típicos incluyen los siguientes:.

  • Puedes compartir una única conexión WebSocket entre varias pestañas, reduciendo el número de conexiones y centralizando las reconexiones.
  • Puedes sincronizar el estado entre pestañas (gestión centralizada para Pub/Sub o almacenes de datos).
  • Puedes serializar operaciones de IndexedDB, mediando el acceso simultáneo.
  • Puedes prevenir la ejecución duplicada de procesos que requieren mucha computación.

Shared Worker tiene un papel diferente al de Service Worker. Service Worker actúa principalmente como un proxy de red, mientras que Shared Worker se enfoca en realizar cálculos arbitrarios o gestión de estado compartida entre páginas.

API básica y ciclo de vida

Creación (hilo principal)

 1// main.js
 2// The second argument 'name' serves as an identifier to share the same SharedWorker
 3// if both the URL and name are the same.
 4const worker = new SharedWorker('/shared-worker.js', { name: 'app-core' });
 5
 6// Get the communication port (MessagePort) with this SharedWorker
 7const port = worker.port;
 8
 9// Receive messages with onmessage, send messages with postMessage
10port.onmessage = (ev) => {
11    console.log('From SharedWorker:', JSON.stringify(ev.data));
12};
13
14// When using onmessage, start() is not required
15// (start() is needed when using addEventListener instead)
16port.postMessage({ type: 'hello', from: location.pathname });
  • Este código muestra cómo crear un Shared Worker y enviar/recibir mensajes a través de su puerto.

Receptor (dentro del contexto del Shared Worker)

 1// shared-worker.js
 2// SharedWorkerGlobalScope has an onconnect event,
 3// and a MessagePort is provided for each connection
 4const ports = new Set();
 5
 6onconnect = (e) => {
 7    const port = e.ports[0];
 8    ports.add(port);
 9
10    port.onmessage = (ev) => {
11        // Log the received message and send a reply back to the sender
12        console.log('From page:', ev.data);
13        port.postMessage({ type: 'ack', received: ev.data });
14    };
15
16    // For handling explicit disconnection from the page (via MessagePort.close())
17    port.onmessageerror = (err) => {
18        console.error('Message error:', err);
19    };
20
21    // Greeting immediately after connection
22    port.postMessage({ type: 'welcome', clientCount: ports.size });
23};
24
25// Explicit termination of the SharedWorker is usually unnecessary
26// (It will be garbage collected when all ports are closed)
  • Este código muestra cómo, dentro del Shared Worker, se recibe un MessagePort para cada conexión y se procesan y responden los mensajes de los clientes.

Ciclo de vida de un Shared Worker

Shared Worker se inicia cuando se establece la primera conexión y puede finalizar cuando se cierra el último puerto. Las conexiones se cierran al recargar o cerrar la página, y el worker se recrea si es necesario.

Shared Worker es un "script de larga duración compartido dentro del navegador". Si no tienes en cuenta el ciclo de vida de un Shared Worker, es más probable que ocurran problemas como fugas de recursos, persistencia de estados obsoletos y reinicios no intencionados.

Pruébalo: Difusión entre pestañas

Esta es una implementación mínima donde varias pestañas se conectan al mismo Shared Worker y cualquier mensaje enviado se retransmite a todas las pestañas.

main.js

 1const worker = new SharedWorker('/shared-worker.js', { name: 'bus' });
 2const port = worker.port;
 3
 4port.onmessage = (ev) => {
 5    const msg = ev.data;
 6    if (msg.type === 'message') {
 7        const li = document.createElement('li');
 8        li.textContent = `[${new Date(msg.at).toLocaleTimeString()}] ${msg.payload}`;
 9        document.querySelector('#messages').appendChild(li);
10    } else if (msg.type === 'ready') {
11        console.log(`Connected. clients=${msg.clients}`);
12    }
13};
14
15document.querySelector('#form').addEventListener('submit', (e) => {
16    e.preventDefault();
17    const input = document.querySelector('#text');
18    port.postMessage({ type: 'publish', payload: input.value });
19    input.value = '';
20});
21
22document.querySelector('#ping').addEventListener('click', () => {
23    port.postMessage({ type: 'ping' });
24});
  • Este código se conecta a un Shared Worker, implementando la recepción/muestra de mensajes, el envío desde un formulario y el envío de un ping al hacer clic en un botón.

shared-worker.js

 1// Minimal pub/sub bus in a SharedWorker
 2const subscribers = new Set();
 3
 4/** Broadcast to all connected ports */
 5function broadcast(message) {
 6    for (const port of subscribers) {
 7        try {
 8            port.postMessage(message);
 9        } catch (e) {
10            // Unsubscribe if sending fails, just in case
11            subscribers.delete(port);
12        }
13    }
14}
15
16onconnect = (e) => {
17    const port = e.ports[0];
18    subscribers.add(port);
19
20    port.onmessage = (ev) => {
21        const msg = ev.data;
22        if (msg && msg.type === 'publish') {
23            broadcast({ type: 'message', payload: msg.payload, at: Date.now() });
24        } else if (msg && msg.type === 'ping') {
25            port.postMessage({ type: 'pong', at: Date.now() });
26        }
27    };
28
29    port.postMessage({ type: 'ready', clients: subscribers.size });
30};
  • Este código implementa una función pub/sub simple dentro del Shared Worker para reenviar mensajes entre varios clientes.

index.html

 1<!doctype html>
 2<html>
 3    <body>
 4        <form id="form">
 5            <input id="text" placeholder="say something" />
 6            <button>Send</button>
 7        </form>
 8        <button id="ping">Ping</button>
 9        <ul id="messages"></ul>
10        <script src="/main.js" type="module"></script>
11    </body>
12</html>
  • Si abres esta página en varias pestañas, los mensajes enviados desde cualquier pestaña se notificarán a las demás.

Diseño de mensajes: solicitud/respuesta e ID de correlación

Cuando varios clientes interactúan con un Shared Worker, normalmente deseas identificar qué respuesta corresponde a cada solicitud. Por lo tanto, es una práctica estándar incluir un ID de correlación para asociar las respuestas con las solicitudes.

main.js

 1function createClient(workerUrl, name) {
 2    const w = new SharedWorker(workerUrl, { name });
 3    const port = w.port;
 4    const pending = new Map(); // id -> {resolve,reject}
 5    let nextId = 1;
 6
 7    port.onmessage = (ev) => {
 8        const { id, ok, result, error } = ev.data || {};
 9        if (!id || !pending.has(id)) return;
10        const { resolve, reject } = pending.get(id);
11        pending.delete(id);
12        ok ? resolve(result) : reject(new Error(error));
13    };
14
15    function call(method, params) {
16        const id = nextId++;
17        return new Promise((resolve, reject) => {
18            pending.set(id, { resolve, reject });
19            port.postMessage({ id, method, params });
20        });
21    }
22
23    return { call };
24}
25
26// usage
27const client = createClient('/shared-worker.js', 'rpc');
28client.call('add', { a: 2, b: 3 }).then(console.log);   // -> 5
29client.call('sleep', { ms: 500 }).then(console.log);     // -> 'done'
  • Este código implementa un cliente RPC sencillo que puede realizar llamadas a métodos asíncronos al Shared Worker. Aquí, un servidor RPC (servidor de llamada a procedimiento remoto) se refiere a un servidor que proporciona un mecanismo para llamar funciones y procedimientos desde otros programas o procesos.
  • En este ejemplo, el id simplemente se incrementa, pero también podría usar un UUID, una cadena aleatoria o combinar una marca de tiempo con un contador para crear una clave única.

shared-worker.js

 1// A small request/response router with correlation ids
 2onconnect = (e) => {
 3    const port = e.ports[0];
 4
 5    port.onmessage = async (ev) => {
 6        const { id, method, params } = ev.data || {};
 7        try {
 8            let result;
 9            switch (method) {
10                case 'add':
11                    result = (params?.a ?? 0) + (params?.b ?? 0);
12                    break;
13                case 'sleep':
14                    await new Promise(r => setTimeout(r, params?.ms ?? 0));
15                    result = 'done';
16                    break;
17                default:
18                    throw new Error(`Unknown method: ${method}`);
19            }
20            port.postMessage({ id, ok: true, result });
21        } catch (err) {
22            port.postMessage({ id, ok: false, error: String(err) });
23        }
24    };
25};
  • Este código implementa un servidor RPC simple que ejecuta el procesamiento según el method y envía el resultado de vuelta junto con el ID de la solicitud.

Patrones de uso típicos

Existen varios patrones prácticos para usar Shared Worker, como los siguientes:.

Multiplexación de un único WebSocket (compartido entre pestañas)

Si cada pestaña abre su propia conexión WebSocket, esto afectará la carga del servidor y el límite de conexiones. Coloca solo un WebSocket en el Shared Worker, y cada pestaña envía/recibe mensajes a través del worker.

main.js

 1const w = new SharedWorker('/shared-worker.js', { name: 'ws' });
 2const port = w.port;
 3
 4port.onmessage = (ev) => {
 5    const msg = ev.data;
 6    if (msg.type === 'data') {
 7        console.log('Server says:', msg.payload);
 8    } else if (msg.type === 'socket') {
 9        console.log('WS state:', msg.state);
10    }
11};
12
13function send(payload) {
14    port.postMessage({ type: 'send', payload });
15}
  • Este código implementa el proceso de enviar/recibir mensajes a un servidor WebSocket y recibir notificaciones del estado de la conexión mediante un Shared Worker.

shared-worker.js

 1let ws;
 2const ports = new Set();
 3
 4function ensureSocket() {
 5    if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
 6        return;
 7    }
 8    ws = new WebSocket('wss://example.com/stream');
 9    ws.onopen = () => broadcast({ type: 'socket', state: 'open' });
10    ws.onclose = () => broadcast({ type: 'socket', state: 'closed' });
11    ws.onerror = (e) => broadcast({ type: 'socket', state: 'error', detail: String(e) });
12    ws.onmessage = (m) => broadcast({ type: 'data', payload: m.data });
13}
14
15function broadcast(msg) {
16    for (const p of ports) p.postMessage(msg);
17}
18
19onconnect = (e) => {
20    const port = e.ports[0];
21    ports.add(port);
22    ensureSocket();
23
24    port.onmessage = (ev) => {
25        const msg = ev.data;
26        if (msg?.type === 'send' && ws?.readyState === WebSocket.OPEN) {
27            ws.send(JSON.stringify(msg.payload));
28        }
29    };
30};
  • Este código implementa un mecanismo dentro del Shared Worker para compartir una sola conexión WebSocket, difundiendo el estado de la conexión y los datos recibidos a todos los clientes, mientras que envía las solicitudes salientes de cada cliente al servidor a través del WebSocket.

  • Al centralizar las estrategias de reconexión y la cola de mensajes no enviados en el Shared Worker, el comportamiento se vuelve consistente en todas las pestañas.

Mediación de IndexedDB (serialización)

Al acceder a la misma base de datos desde varias pestañas, si quieres evitar conflictos por transacciones simultáneas o esperas por bloqueos, puedes ponerlas en cola en el Shared Worker y procesarlas de forma serial.

 1// db-worker.js (very simplified – error handling omitted)
 2let db;
 3async function openDB() {
 4    if (db) return db;
 5    db = await new Promise((resolve, reject) => {
 6        const req = indexedDB.open('appdb', 1);
 7        req.onupgradeneeded = () => req.result.createObjectStore('kv');
 8        req.onsuccess = () => resolve(req.result);
 9        req.onerror = () => reject(req.error);
10    });
11    return db;
12}
13
14async function put(key, value) {
15    const d = await openDB();
16    return new Promise((resolve, reject) => {
17        const tx = d.transaction('kv', 'readwrite');
18        tx.objectStore('kv').put(value, key);
19        tx.oncomplete = () => resolve(true);
20        tx.onerror = () => reject(tx.error);
21    });
22}
23
24async function get(key) {
25    const d = await openDB();
26    return new Promise((resolve, reject) => {
27        const tx = d.transaction('kv', 'readonly');
28        const req = tx.objectStore('kv').get(key);
29        req.onsuccess = () => resolve(req.result);
30        req.onerror = () => reject(req.error);
31    });
32}
33
34onconnect = (e) => {
35    const port = e.ports[0];
36    port.onmessage = async (ev) => {
37        const { id, op, key, value } = ev.data;
38        try {
39            const result = op === 'put' ? await put(key, value) : await get(key);
40            port.postMessage({ id, ok: true, result });
41        } catch (err) {
42            port.postMessage({ id, ok: false, error: String(err) });
43        }
44    };
45};
  • Este código implementa un mecanismo donde las operaciones de lectura/escritura en IndexedDB se ponen en cola a través del Shared Worker, serializando el acceso desde varias pestañas para evitar conflictos y contención de bloqueos.

Consejos sobre modularización y empaquetadores

Si deseas escribir tu script de Shared Worker como un módulo ES, el comportamiento y la compatibilidad pueden variar según el entorno. En la práctica, es más seguro elegir una de las siguientes opciones:.

  • Escribe en formato clásico de worker y utiliza importScripts() para cargar las dependencias si es necesario.
  • Utiliza la opción de entrada de worker de tu empaquetador (como Vite / Webpack / esbuild, etc.) y genera un paquete separado para el Shared Worker en tiempo de compilación.

Consejos para manejo de errores, detección de desconexión y robustez

Considera los siguientes puntos para el manejo de errores y la robustez:.

  • Manejo de envíos antes de que se establezca la conexión Pon en cola los mensajes que llegan antes de que el puerto esté listo.

  • Detección de desconexión MessagePort no tiene un controlador estándar onclose. En el lado principal, envíe un mensaje {type: 'bye'} con port.postMessage durante beforeunload, o de lo contrario, especifique claramente en el protocolo para asegurarse de que el worker se cierre correctamente.

  • Reconexión Cuando se recarga una página o se vuelve a abrir una pestaña, se crea un nuevo puerto. Prepara un mensaje de sincronización inicial (para enviar el estado completo de una vez).

  • Presión inversa Durante una difusión intensiva, cambia a limitaciones/debouncing o envío de instantáneas.

  • Seguridad Un Shared Worker se comparte fundamentalmente dentro del mismo origen. Si colocas secretos dentro del worker, considera diseñar una verificación adicional con tokens u otros mecanismos similares para el lado llamante.

Cómo usar Dedicated Worker, Shared Worker y Service Worker de forma adecuada

Cada tipo de Worker tiene las siguientes características:.

  • Dedicated Worker Dedicated Worker está destinado a ser utilizado sólo por una única página. Permite una separación 1:1 de los cálculos respecto a la interfaz de usuario.

  • Shared Worker Shared Worker puede ser compartido entre varias páginas con el mismo origen. Es ideal para la comunicación entre pestañas y compartir una sola conexión.

  • Service Worker Service Worker puede ser usado para funciones de proxy en red, almacenamiento en caché, operaciones fuera de línea, notificaciones push y sincronización en segundo plano. Su fortaleza es la capacidad de interceptar solicitudes fetch.

Como regla general: use Shared Worker para 'compartir información y procesamiento arbitrario entre pestañas', Service Worker para 'control de red', y Dedicated Worker si solo quiere descargar procesamiento pesado de la interfaz de usuario.

Errores comunes

Al usar un Shared Worker, debes tener cuidado con los siguientes puntos.

  • Olvidar llamar a start() o invocaciones innecesarias Cuando uses port.addEventListener('message', ...), debes llamar a port.start(). Esto no es necesario si utilizas port.onmessage = ....

  • Difusión sin restricciones La carga aumenta a medida que crece el número de pestañas. Puedes reducir la carga implementando envío diferencial o temas de suscripción (filtrado por tema).

  • Costo de copia de objetos postMessage duplica los datos. Para datos grandes, considera enviarlos como Transferible (por ejemplo, ArrayBuffer) o usar memoria compartida (SharedArrayBuffer) junto con Atomics.

  • Ciclo de vida y reinicialización Shared Worker puede finalizar cuando el último cliente se desconecta. Diseña adecuadamente la inicialización para las primeras conexiones y reinicios para evitar efectos secundarios y errores.

Resumen

  • Shared Worker es un lugar para implementar lógica personalizada de larga duración compartida entre varias páginas.
  • Aclara el contrato de mensajes (tipos/protocolos) y haz robusto tu diseño de solicitudes/respuestas añadiendo IDs de correlación.
  • Es ideal para procesos que funcionan mejor cuando se unifican entre todas las pestañas, como multiplexar conexiones WebSocket o serializar el acceso a IndexedDB.
  • Ajustar detalles como los empaquetadores, definiciones de tipos y la reconexión puede mejorar enormemente la mantenibilidad y la experiencia de usuario.

Puedes seguir el artículo anterior utilizando Visual Studio Code en nuestro canal de YouTube. Por favor, también revisa nuestro canal de YouTube.

YouTube Video