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 & 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 deService Worker
.Service Worker
actúa principalmente como un proxy de red, mientras queShared 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 unMessagePort
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ándaronclose
. En el lado principal, envíe un mensaje{type: 'bye'}
conport.postMessage
durantebeforeunload
, 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', yDedicated 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 usesport.addEventListener('message', ...)
, debes llamar aport.start()
. Esto no es necesario si utilizasport.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 conAtomics
. -
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.