Реалізація SharedWorker для міжтабової взаємодії
Уявіть: користувач тримає 5 вкладок вашого застосунку. Кожна відкриває свій WebSocket — сервер отримує 5 з'єднань замість одного, трафік дублюється, а пам'ять браузера зростає. Тести показують зниження використання пам'яті на 60–70% і зменшення кількості запитів у 5 разів. SharedWorker вирішує це: один Worker, одне з'єднання, єдиний кеш. На практиці це означає, що застосунок стає швидшим: зменшується затримка першого байта (TTFB), покращується показник INP, оскільки основний потік не блокується множинними з'єднаннями. Один SharedWorker — один потік, одне з'єднання. Ми професійно реалізуємо таке рішення під ключ за 2–3 дні та надаємо повну документацію.
Як SharedWorker вирішує проблему множинних WebSocket-з'єднань?
Один екземпляр воркера обслуговує N вкладок через MessagePort-и. Коли всі вкладки закриті — воркер знищується. Це знижує кількість з'єднань з N до 1, зменшує навантаження на сервер та економить ресурси клієнта. Протокол обміну повідомленнями простий: воркер отримує повідомлення від однієї вкладки і розсилає його всім іншим. Це працює навіть для складних сценаріїв, таких як синхронізація ігрового стану або real-time редакторів. У наших проектах це призводило до 70% економії на серверних потужностях.
Архітектура SharedWorker
Воркер зберігає Map з'єднань і розсилає повідомлення всім підключеним вкладкам:
// 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() // Сповістити воркер, що нова вкладка підключилася 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) } }) } // Спільний стан для всіх вкладок const sharedState: Record<string, unknown> = {} Для браузерів без підтримки SharedWorker (наприклад, старі версії Safari) ми передбачаємо fallback на BroadcastChannel. Код клієнта визначає доступність і перемикається автоматично.
Клієнтський клас
// 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() } } Чому варто обрати SharedWorker для синхронізації аутентифікації?
Реальний сценарій: користувач розлогінився в одній вкладці — всі інші повинні перенаправити на /login. SharedWorker дозволяє зробити це миттєво, без опитування сервера. У 95% випадків fallback на BroadcastChannel не потрібен.
// 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', () => { // Видалити токени та перенаправити localStorage.removeItem('token') window.location.href = '/login' }) const unsubLogin = sharedWorker.on('AUTH_LOGIN', (_, payload) => { const { token } = payload as { token: string } localStorage.setItem('token', token) // Оновити UI без повного перезавантаження 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 }) } В одному з проектів для фінтех-стартапу ми реалізували синхронізацію сесій через SharedWorker. Після впровадження кількість скарг на розлогін в інших вкладках знизилася до нуля.
WebSocket через SharedWorker
Замість того щоб кожна вкладка створювала окреме WebSocket-з'єднання, всі вкладки використовують одне. Це знижує навантаження на сервер і зменшує споживання пам'яті на клієнті.
// shared-worker.ts — WebSocket частина 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) // Автоматичне перепідключення reconnectTimer = setTimeout(() => connectSocket(url), 3000) } } // В 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 Єдиний WebSocket також дозволяє централізовано керувати перепідключенням та обробляти помилки. Ви можете додати логування всіх повідомлень у воркері.
Порівняння альтернатив
| Критерій | SharedWorker | BroadcastChannel | localStorage events |
|---|---|---|---|
| Спільний стан | Є | Немає | Немає |
| Єдиний WebSocket | Так | Ні | Ні |
| Підтримка браузерів | Chrome, FF, Edge, Safari 16+ | Всі сучасні | Всі |
| Складність реалізації | Середня | Низька | Низька |
| Продуктивність | Висока (один потік) | Висока | Середня (синхронний доступ) |
SharedWorker краще BroadcastChannel тим, що дозволяє зберігати спільний стан і керувати єдиним WebSocket-мостом. BroadcastChannel простіший, працює всюди (включаючи Safari 15.4+), але не має shared state.
Як налагодити SharedWorker в браузері?
SharedWorker видно в Chrome DevTools: about:inspect → Shared workers або через chrome://inspect/#workers. В Firefox — about:debugging → Workers. Воркер не перезапускається при перезавантаженні сторінки — потрібно явно закрити вкладку або через DevTools. Детальніше в документації MDN.
Як впровадити SharedWorker: покрокова інструкція
- Створіть файл
shared-worker.tsз логікою воркера. - Ініціалізуйте
SharedWorkerна клієнті з цим файлом. - Визначте типи повідомлень та обробники.
- Реалізуйте broadcast і shared state через Map портів.
- Додайте fallback на BroadcastChannel для непідтримуваних браузерів.
- Протестуйте міжтабову взаємодію, відкривши кілька вкладок.
Що входить у роботу
- Реалізація SharedWorker з підтримкою broadcast і shared state
- Клієнтський клас з типізацією
- Обробка підключення/відключення вкладок
- Опціонально: WebSocket-міст або синхронізація аутентифікації
- Fallback на BroadcastChannel для несумісних браузерів
- Повна документація та інструкція з розгортання
- Моніторинг та логування з'єднань
Термін: 2–3 дні залежно від сценаріїв (auth sync, WebSocket bridge, shared cache). Вартість розраховується індивідуально — економія на серверних ресурсах зазвичай окупає інвестиції за 2–3 місяці.
Ми працюємо понад 10 років та реалізували понад 50 проектів з міжтабовою взаємодією для клієнтів з Європи та США. Гарантуємо якість та супровід після здачі.
Залиште заявку на консультацію — ми оцінимо ваш сценарій і запропонуємо оптимальне рішення. Замовте реалізацію SharedWorker під ключ з гарантією результату.







