Shared Worker in JavaScript
This article explains the Shared Worker
in JavaScript.
We will explain everything from the basics of Shared Worker
to practical use cases step by step.
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 in JavaScript
What is a Shared Worker
?
A Shared Worker
is a worker thread that can be shared among multiple pages (tabs, windows, iframes, etc.) within the same origin. Unlike a page-specific Dedicated Worker
, the main feature is that a single background process can be shared among multiple pages. Typical use cases include the following:.
- You can share a single WebSocket connection across multiple tabs, reducing the number of connections and centralizing reconnections.
- You can synchronize state between tabs (centralized management for Pub/Sub or stores).
- You can serialize IndexedDB operations, mediating simultaneous access.
- You can prevent duplicate execution of computationally expensive processes.
Shared Worker
has a different role fromService Worker
.Service Worker
mainly acts as a network proxy, whileShared Worker
focuses on performing arbitrary calculations or state management shared across pages.
Basic API and Lifecycle
Creating (Main Thread)
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 });
- This code shows how to create a
Shared Worker
and send/receive messages through its port.
Receiver (within the Shared Worker
scope)
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)
- This code demonstrates how, inside the
Shared Worker
, aMessagePort
is received for each connection and processes and replies to messages from clients.
Lifecycle of a Shared Worker
Shared Worker
starts when the first connection is established and may terminate when the last port is closed. Connections are closed upon page reload/close, and the worker is recreated if needed.
Shared Worker
is a "long-lived script shared within the browser". If you are not mindful of the Shared Worker
lifecycle, issues such as resource leaks, stale state persistence, and unintended restarts are more likely to occur.
Try It Out: Broadcast Between Tabs
This is a minimal implementation where multiple tabs connect to the same Shared Worker
and any message sent is broadcast to all tabs.
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});
- This code connects to a
Shared Worker
, implementing message reception/display, posting from a send form, and sending a ping by clicking a button.
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};
- This code implements a simple pub/sub feature inside the
Shared Worker
to relay messages between multiple clients.
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>
- If you open this page in multiple tabs, messages sent from any tab will be notified to the others.
Message Design: Request/Response and Correlation ID
When multiple clients interact with a Shared Worker
, you often want to identify which response corresponds to which request. Therefore, it is standard practice to include a correlation ID to associate responses with requests.
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'
- This code implements a simple RPC client that can make asynchronous method calls to the
Shared Worker
. Here, an RPC server (Remote Procedure Call server) refers to a server that provides a mechanism for calling functions and procedures from other programs or processes. - In this example, the
id
is simply incremented, but you could also use a UUID, a random string, or combine a timestamp with a counter to create a unique key.
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};
- This code implements a simple RPC server that executes processing according to the
method
and sends the result back together with the request ID.
Typical Usage Patterns
There are several practical patterns when using Shared Worker
, such as the following:.
Multiplexing a Single WebSocket (Shared Across Tabs)
If each tab opens its own WebSocket connection, it will affect the server load and the connection limit. Place just one WebSocket in the Shared Worker
, and each tab sends/receives messages via the 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}
- This code implements the process of sending/receiving messages to a WebSocket server and receiving connection status notifications via a
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};
-
This code implements a mechanism inside the
Shared Worker
to share a single WebSocket connection, broadcasting connection status and received data to all clients, while sending each client's outgoing requests to the server via the WebSocket. -
By centralizing reconnection strategies and unsent message queuing in the
Shared Worker
, behavior becomes consistent across all tabs.
IndexedDB Mediation (Serialization)
When accessing the same DB from multiple tabs, if you want to avoid conflicts from simultaneous transactions or lock waiting, you can queue them in the Shared Worker
and process them serially.
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};
- This code implements a mechanism where read/write operations to IndexedDB are queued via the
Shared Worker
, serializing access from multiple tabs to avoid conflicts and lock contention.
Modularization and Bundler Tips
If you want to write your Shared Worker
script as an ES Module, behavior and support can vary by environment. In practice, it is safer to choose one of the following:.
- Write in classic worker format, and use
importScripts()
to load dependencies if needed. - Use the worker entry feature of your bundler (such as Vite / Webpack / esbuild, etc.), and create a separate bundle for the
Shared Worker
at build time.
Tips for Error Handling, Disconnection Detection, and Robustness
Consider the following points for error handling and robustness:.
-
Handling Sends Before Connection is Established Queue messages that arrive before the port is ready.
-
Disconnection Detection
MessagePort
does not have a standardonclose
handler. On the main side, send a{type: 'bye'}
message withport.postMessage
duringbeforeunload
, or otherwise clearly specify in the protocol to ensure the worker is cleaned up. -
Reconnection When a page is reloaded or a tab is reopened, a new port is created. Prepare an initial synchronization message (to send the complete state at once).
-
Backpressure During heavy broadcasting, switch to throttling/debouncing or sending snapshots.
-
Security A
Shared Worker
is fundamentally shared within the same origin. If placing secrets inside the worker, consider designing side verification with tokens or similar means for the calling side.
How to Use Dedicated Worker
, Shared Worker
, and Service Worker
Appropriately
Each type of Worker has the following characteristics:.
-
Dedicated Worker
Dedicated Worker
is intended for use by a single page only. It enables a 1:1 separation of computations from the UI. -
Shared Worker
Shared Worker
can be shared across multiple pages with the same origin. It is ideal for inter-tab communication and sharing a single connection. -
Service Worker
Service Worker
can be used for network proxying, caching, offline operations, push notifications, and background synchronization. Its strength is the ability to intercept fetch requests.
As a rule of thumb: use
Shared Worker
for 'information sharing and arbitrary processing between tabs',Service Worker
for 'network control', andDedicated Worker
if you just want to offload heavy processing from the UI.
Common Pitfalls
When using a Shared Worker
, you need to be careful about the following points.
-
Forgetting to call
start()
or unnecessary invocations When usingport.addEventListener('message', ...)
, you must callport.start()
. This is unnecessary if you useport.onmessage = ...
. -
Unrestricted Broadcasting The load increases as the number of tabs increases. You can reduce the load by implementing differential delivery or subscription topics (filtering by topic).
-
Copy Cost of Objects
postMessage
duplicates the data. For large data, consider sending it as Transferable (such asArrayBuffer
) or using shared memory (SharedArrayBuffer
) together withAtomics
. -
Lifetime and Re-initialization
Shared Worker
may terminate when the last client disconnects. Properly design initialization for first connections and restarts to avoid side effects and bugs.
Summary
Shared Worker
is a place to implement long-lived custom logic shared across multiple pages.- Clarify the message contract (types/protocols) and make your request/response design robust by attaching correlation IDs.
- It is ideal for processes that work better when unified across all tabs, such as multiplexing WebSocket connections or serializing access to IndexedDB.
- Fine-tuning details such as bundlers, type definitions, and reconnection can greatly improve maintainability and user experience.
You can follow along with the above article using Visual Studio Code on our YouTube channel. Please also check out the YouTube channel.