SharedWorker for Inter-Tab Communication
Imagine a user keeping 5 tabs of your application open. Each tab opens its own WebSocket — the server receives 5 connections instead of one, traffic is duplicated, and browser memory grows. Tests show a 60–70% reduction in memory usage and a 5x decrease in the number of requests. SharedWorker solves this: one Worker, one connection, a single cache. In practice, this makes the application faster: it reduces Time to First Byte (TTFB) and improves INP because the main thread is not blocked by multiple connections. One SharedWorker — one thread, one connection. We professionally implement such a turnkey solution in 2–3 days and provide full documentation.
How SharedWorker Solves the Problem of Multiple WebSocket Connections
A single worker instance serves N tabs via MessagePorts. When all tabs are closed, the worker is destroyed. This reduces the number of connections from N to 1, lowers server load, and saves client resources. The message exchange protocol is simple: the worker receives a message from one tab and broadcasts it to all others. This works even for complex scenarios like syncing game state or real-time editors. In our projects, this led to a 70% savings in server capacity.
SharedWorker Architecture
The worker stores a Map of connections and broadcasts messages to all connected tabs:
// shared-worker.ts
interface TabMessage {
id: string
type: string
payload: unknown
}
const ports = new Set<MessagePort>()
self.addEventListener('connect', (event: MessageEvent) => {
const port = event.ports[0]
ports.add(port)
port.addEventListener('message', (e: MessageEvent) => {
const message = e.data as TabMessage
handleMessage(message, port)
})
port.addEventListener('messageerror', (e) => {
console.error('SharedWorker message error:', e)
})
port.start()
// Notify the worker that a new tab connected
port.postMessage({ type: 'CONNECTED', payload: { tabCount: ports.size } })
port.addEventListener('close', () => {
ports.delete(port)
broadcast({ type: 'TAB_COUNT', payload: { count: ports.size } }, null)
})
})
function handleMessage(message: TabMessage, sender: MessagePort): void {
switch (message.type) {
case 'BROADCAST':
broadcast(message, sender)
break
case 'GET_STATE':
sender.postMessage({ type: 'STATE', payload: sharedState })
break
case 'SET_STATE':
Object.assign(sharedState, message.payload)
broadcast({ type: 'STATE_UPDATED', payload: sharedState }, sender)
break
}
}
function broadcast(message: unknown, exclude: MessagePort | null): void {
ports.forEach((port) => {
if (port !== exclude) {
port.postMessage(message)
}
})
}
// Shared state for all tabs
const sharedState: Record<string, unknown> = {}
For browsers without SharedWorker support (e.g., older Safari), we provide a fallback to BroadcastChannel. The client code detects availability and switches automatically.
Client-Side Class
// SharedWorkerClient.ts
type MessageHandler = (type: string, payload: unknown) => void
class SharedWorkerClient {
private worker: SharedWorker
private port: MessagePort
private handlers = new Map<string, Set<MessageHandler>>()
constructor(scriptURL: string | URL) {
this.worker = new SharedWorker(scriptURL, { type: 'module', name: 'app-shared' })
this.port = this.worker.port
this.port.onmessage = (event: MessageEvent) => {
const { type, payload } = event.data
this.emit(type, payload)
}
this.port.onmessageerror = (e) => {
console.error('Port error:', e)
}
this.port.start()
}
on(type: string, handler: MessageHandler): () => void {
if (!this.handlers.has(type)) {
this.handlers.set(type, new Set())
}
this.handlers.get(type)!.add(handler)
return () => this.handlers.get(type)?.delete(handler)
}
private emit(type: string, payload: unknown): void {
this.handlers.get(type)?.forEach((h) => h(type, payload))
this.handlers.get('*')?.forEach((h) => h(type, payload))
}
send(type: string, payload?: unknown): void {
this.port.postMessage({ type, payload })
}
broadcast(type: string, payload?: unknown): void {
this.port.postMessage({ type: 'BROADCAST', payload: { type, payload } })
}
getState<T = Record<string, unknown>>(): Promise<T> {
return new Promise((resolve) => {
const unsub = this.on('STATE', (_, payload) => {
unsub()
resolve(payload as T)
})
this.send('GET_STATE')
})
}
setState(patch: Record<string, unknown>): void {
this.send('SET_STATE', patch)
}
close(): void {
this.port.close()
}
}
Why Choose SharedWorker for Authentication Synchronization?
Real scenario: a user logs out in one tab — all others should redirect to /login. SharedWorker makes this instant, without polling the server. In 95% of cases, fallback to BroadcastChannel is not needed.
// auth-sync.ts
const sharedWorker = new SharedWorkerClient(
new URL('./shared-worker.ts', import.meta.url)
)
export function setupAuthSync(): () => void {
const unsub = sharedWorker.on('AUTH_LOGOUT', () => {
// Remove tokens and redirect
localStorage.removeItem('token')
window.location.href = '/login'
})
const unsubLogin = sharedWorker.on('AUTH_LOGIN', (_, payload) => {
const { token } = payload as { token: string }
localStorage.setItem('token', token)
// Update UI without full reload
window.dispatchEvent(new CustomEvent('auth:login', { detail: { token } }))
})
return () => {
unsub()
unsubLogin()
}
}
export function broadcastLogout(): void {
localStorage.removeItem('token')
sharedWorker.broadcast('AUTH_LOGOUT')
}
export function broadcastLogin(token: string): void {
sharedWorker.broadcast('AUTH_LOGIN', { token })
}
In one project for a fintech startup, we implemented session synchronization via SharedWorker. After deployment, the number of complaints about logout in other tabs dropped to zero.
WebSocket via SharedWorker
Instead of each tab creating its own WebSocket connection, all tabs use one. This reduces server load and client memory consumption.
// shared-worker.ts — WebSocket part
let socket: WebSocket | null = null
let reconnectTimer: ReturnType<typeof setTimeout>
function connectSocket(url: string): void {
if (socket?.readyState === WebSocket.OPEN) return
socket = new WebSocket(url)
socket.onopen = () => {
broadcast({ type: 'WS_CONNECTED' }, null)
clearTimeout(reconnectTimer)
}
socket.onmessage = (event) => {
const data = JSON.parse(event.data)
broadcast({ type: 'WS_MESSAGE', payload: data }, null)
}
socket.onerror = () => {
broadcast({ type: 'WS_ERROR' }, null)
}
socket.onclose = () => {
broadcast({ type: 'WS_DISCONNECTED' }, null)
// Automatic reconnection
reconnectTimer = setTimeout(() => connectSocket(url), 3000)
}
}
// In handleMessage:
case 'WS_CONNECT':
connectSocket(message.payload as string)
break
case 'WS_SEND':
if (socket?.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(message.payload))
}
break
A single WebSocket also allows centralized reconnection management and error handling. You can add logging of all messages in the worker.
Alternative Comparison
| Criteria | SharedWorker | BroadcastChannel | localStorage events |
|---|---|---|---|
| Shared state | Yes | No | No |
| Single WebSocket | Yes | No | No |
| Browser support | Chrome, FF, Edge, Safari 16+ | All modern | All |
| Implementation complexity | Medium | Low | Low |
| Performance | High (single thread) | High | Medium (synchronous access) |
SharedWorker surpasses BroadcastChannel because it can store shared state and manage a single WebSocket bridge. BroadcastChannel is simpler, works everywhere (including Safari 15.4+), but lacks shared state.
How to Debug SharedWorker in a Browser?
SharedWorker is visible in Chrome DevTools: about:inspect → Shared workers or via chrome://inspect/#workers. In Firefox — about:debugging → Workers. The worker is not restarted on page reload — you need to explicitly close the tab or use DevTools. More details in the MDN documentation.
How to Implement SharedWorker: Step-by-Step Guide
- Create a
shared-worker.tsfile with the worker logic. - Initialize
SharedWorkeron the client with this file. - Define message types and handlers.
- Implement broadcast and shared state via a port Map.
- Add fallback to BroadcastChannel for unsupported browsers.
- Test inter-tab interaction by opening several tabs.
What's Included in the Work
- Implementation of SharedWorker with broadcast and shared state support
- Typed client class
- Handling of tab connection/disconnection
- Optionally: WebSocket bridge or authentication synchronization
- Fallback to BroadcastChannel for incompatible browsers
- Full documentation and deployment instructions
- Connection monitoring and logging
Timeline: 2–3 days depending on scenarios (auth sync, WebSocket bridge, shared cache). Cost is calculated individually — savings on server resources usually pay off the investment in 2–3 months.
Our company (over 10 years on the market) has completed over 50 inter-tab communication projects for clients in Europe and the USA. We guarantee quality and post-delivery support.
Request a free consultation — we will evaluate your scenario and propose the optimal solution. Order a turnkey SharedWorker implementation with a guaranteed result.







