Shared Worker em JavaScript
Este artigo explica o Shared Worker
em JavaScript.
Vamos explicar tudo, desde os fundamentos do Shared Worker
até casos de uso práticos, passo a passo.
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>
Shared Worker em JavaScript
O que é um Shared Worker
?
Um Shared Worker
é uma thread de execução que pode ser compartilhada entre várias páginas (abas, janelas, iframes, etc.) dentro da mesma origem. Ao contrário de um Dedicated Worker
específico para cada página, a principal característica é que um único processo em segundo plano pode ser compartilhado entre várias páginas. Os casos de uso típicos incluem o seguinte:.
- Você pode compartilhar uma única conexão WebSocket entre várias abas, reduzindo o número de conexões e centralizando tentativas de reconexão.
- Você pode sincronizar o estado entre abas (gestão centralizada para Pub/Sub ou stores).
- Você pode serializar operações do IndexedDB, mediando acessos simultâneos.
- Você pode prevenir a execução duplicada de processos computacionalmente custosos.
Shared Worker
tem um papel diferente doService Worker
.Service Worker
atua principalmente como um proxy de rede, enquanto oShared Worker
foca em executar cálculos arbitrários ou gerenciar estados compartilhados entre várias páginas.
API Básica e Ciclo de Vida
Criação (Thread 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 mostra como criar um
Shared Worker
e enviar/receber mensagens através da sua porta.
Receptor (dentro do escopo do 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 demonstra como, dentro do
Shared Worker
, umMessagePort
é recebido para cada conexão, processando e respondendo às mensagens dos clientes.
Ciclo de Vida de um Shared Worker
O Shared Worker
inicia quando a primeira conexão é estabelecida e pode encerrar quando a última porta é fechada. As conexões são fechadas ao recarregar ou fechar a página, e o worker é recriado se necessário.
Shared Worker
é um "script de longa duração compartilhado no navegador". Se você não estiver atento ao ciclo de vida do Shared Worker
, problemas como vazamento de recursos, persistência de estado desatualizado e reinicializações inesperadas são mais propensos a ocorrer.
Experimente: Broadcast Entre Abas
Esta é uma implementação mínima onde várias abas se conectam ao mesmo Shared Worker
e qualquer mensagem enviada é transmitida para todas as abas.
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 um
Shared Worker
, implementando recepção/exibição de mensagens, postagem por meio de um formulário de envio e envio de ping ao clicar em um botão.
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 um recurso simples de pub/sub dentro do
Shared Worker
para repassar mensagens entre vários 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>
- Se você abrir esta página em várias abas, as mensagens enviadas de qualquer aba serão notificadas às demais.
Design de Mensagem: Request/Response e Correlation ID
Quando vários clientes interagem com um Shared Worker
, geralmente você deseja identificar qual resposta corresponde a qual solicitação. Portanto, é uma prática padrão incluir um ID de correlação para associar respostas aos pedidos.
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 um cliente RPC simples que pode fazer chamadas de método assíncronas ao
Shared Worker
. Aqui, um servidor RPC (Remote Procedure Call) refere-se a um servidor que fornece um mecanismo para chamar funções e procedimentos de outros programas ou processos. - Neste exemplo, o
id
é simplesmente incrementado, mas você também pode usar um UUID, uma string aleatória ou combinar um timestamp com um contador para criar uma chave ú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 um servidor RPC simples que executa o processamento de acordo com o
method
e envia o resultado de volta junto com o ID da requisição.
Padrões de Uso Típicos
Existem diversos padrões práticos ao usar Shared Worker
, como os seguintes:.
Multiplexação de um Único WebSocket (Compartilhado entre Abas)
Se cada aba abrir sua própria conexão WebSocket, isso afetará a carga do servidor e o limite de conexões. Coloque apenas um WebSocket no Shared Worker
, e cada aba envia/recebe mensagens através do 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 o processo de envio/recebimento de mensagens para um servidor WebSocket e recebimento de notificações de status de conexão via um
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 um mecanismo dentro do
Shared Worker
para compartilhar uma única conexão WebSocket, transmitindo o status da conexão e dados recebidos para todos os clientes, enquanto envia as solicitações de saída de cada cliente para o servidor via WebSocket. -
Ao centralizar as estratégias de reconexão e a fila de mensagens não enviadas no
Shared Worker
, o comportamento se torna consistente entre todas as abas.
Mediação do IndexedDB (Serialização)
Ao acessar o mesmo banco de dados a partir de várias abas, se quiser evitar conflitos de transações simultâneas ou espera por bloqueios, você pode enfileirá-las no Shared Worker
e processá-las 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 um mecanismo onde operações de leitura/escrita no IndexedDB são enfileiradas via
Shared Worker
, serializando o acesso de várias abas para evitar conflitos e contenção de bloqueios.
Dicas de Modularização e Bundling
Se você deseja escrever seu script de Shared Worker
como um ES Module, o comportamento e o suporte podem variar de acordo com o ambiente. Na prática, é mais seguro optar por uma das seguintes opções:.
- Escreva no formato clássico de worker e use
importScripts()
para carregar dependências, se necessário. - Use o recurso de entrada de worker do seu bundler (como Vite / Webpack / esbuild, etc.) e crie um bundle separado para o
Shared Worker
no momento da build.
Dicas para Tratamento de Erros, Detecção de Desconexão e Robustez
Considere os seguintes pontos para tratamento de erros e robustez:.
-
Gerenciamento de envios antes da conexão ser estabelecida Coloque em fila as mensagens que chegam antes da porta estar pronta.
-
Detecção de Desconexão O
MessagePort
não possui um handler padrãoonclose
. No lado principal, envie uma mensagem{type: 'bye'}
usandoport.postMessage
durante obeforeunload
ou especifique claramente no protocolo para garantir que o worker seja finalizado. -
Reconexão Quando uma página é recarregada ou uma aba é reaberta, uma nova porta é criada. Prepare uma mensagem de sincronização inicial (para enviar todo o estado de uma vez).
-
Pressão Reversa (Backpressure) Durante broadcasts intensos, alterne para throttling/debouncing ou envio de snapshots.
-
Segurança Um
Shared Worker
é fundamentalmente compartilhado dentro da mesma origem. Se for colocar segredos no worker, considere projetar uma verificação lateral com tokens ou meios similares para o lado que faz a chamada.
Como usar corretamente Dedicated Worker
, Shared Worker
e Service Worker
Cada tipo de Worker possui as seguintes características:.
-
Dedicated Worker
Dedicated Worker
é destinado ao uso por uma única página. Permite uma separação 1:1 dos cálculos em relação à interface do usuário (UI). -
Shared Worker
Shared Worker
pode ser compartilhado entre múltiplas páginas com a mesma origem. É ideal para comunicação entre abas e compartilhamento de uma única conexão. -
Service Worker
Service Worker
pode ser utilizado para proxy de rede, cache, operações offline, notificações push e sincronização em segundo plano. Seu ponto forte é a capacidade de interceptar requisições fetch.
Como regra geral: use
Shared Worker
para 'compartilhamento de informações e processamento arbitrário entre abas',Service Worker
para 'controle de rede', eDedicated Worker
se quiser apenas remover processamento pesado da interface de usuário.
Erros comuns
Ao usar um Shared Worker
, é preciso ter atenção aos seguintes pontos.
-
Esquecer de chamar
start()
ou invocá-lo desnecessariamente Ao usarport.addEventListener('message', ...)
, é obrigatório chamarport.start()
. Isso não é necessário se você usarport.onmessage = ...
. -
Broadcasting irrestrito A carga aumenta conforme cresce o número de abas. Você pode reduzir a carga implementando entrega diferencial ou tópicos de assinatura (filtragem por tópico).
-
Custo de cópia de objetos
postMessage
duplica os dados. Para grandes volumes de dados, considere enviá-los como Transferable (por exemplo,ArrayBuffer
) ou usando memória compartilhada (SharedArrayBuffer
) juntamente comAtomics
. -
Ciclo de vida e reinicialização
Shared Worker
pode ser encerrado quando o último cliente se desconecta. Projete corretamente a inicialização para as primeiras conexões e reinicializações, a fim de evitar efeitos colaterais e bugs.
Resumo
Shared Worker
é um lugar para implementar lógica personalizada de longa duração compartilhada entre várias páginas.- Esclareça o contrato de mensagens (tipos/protocolos) e torne seu design de requisição/resposta robusto anexando IDs de correlação.
- É ideal para processos que funcionam melhor quando unificados em todas as abas, como multiplexação de conexões WebSocket ou serialização de acessos ao IndexedDB.
- Ajustes finos em detalhes como empacotadores (bundlers), definições de tipos e reconexão podem melhorar muito a manutenibilidade e a experiência do usuário.
Você pode acompanhar o artigo acima usando o Visual Studio Code em nosso canal do YouTube. Por favor, confira também o canal do YouTube.