Розробка систем реального часу: WebRTC, SSE, WebSocket
На одному проекті — платформі для онлайн-аукціонів — використовували поллінг кожні 2 секунди. Під нагрузкою від 400 одночасних учасників аукціону сервер отримував 12 000 HTTP-запитів на хвилину тільки для того, щоб дізнатись поточну ставку. 90% відповідей повертали одні й ті ж дані. Перехід на WebSocket знизив нагрузку в 15 разів без зміни UX.
Вибір транспорту під задачу
Три основних механізми реального часу відрізняються не тільки технічно, але й операційно.
Server-Sent Events працюють поверх звичайного HTTP/1.1 або HTTP/2. Браузер відкриває з'єднання, сервер тримає його відкритим та пушить події у форматі text/event-stream. Автоматичне переподключення вбудоване в браузер — не потрібно писати логіку reconnect. Обмеження: тільки сервер → клієнт. Ідеально для нотифікацій, прогресу довгих задач, live-фідів.
WebSocket — повнодуплексний канал після HTTP Upgrade-рукопожиття. Браузер та сервер обмінюються фреймами в обидві сторони. Підходить для чатів, спільного редагування, ігор, торговельних терміналів. Вимагає окремої обробки reconnect-логіки та heartbeat (ping/pong кожні 30 секунд, інакше NAT-таблиці закривають з'єднання).
WebRTC — peer-to-peer аудіо/відео та дані між браузерами безпосередньо, минуючи сервер. Сервер потрібен тільки для сигналізації (STUN/TURN для обходу NAT). На практиці TURN-сервер потрібен приблизно в 20–30% випадків (корпоративні мережі, симетричний NAT), це важливо враховувати у інфраструктурі.
Спільне редагування — найскладніша частина
Collaborative editing — це не просто "хто останній записав, той й правий". Це Operational Transformation або CRDT (Conflict-free Replicated Data Types).
Без алгоритму злиття колізій: два користувачі редагують документ одночасно, обидва вставляють текст у позицію 45. Перший зберігає — позиція зміщується. Другий зберігає поверх — його операція застосовується до застарілого стану. Текст або дублюється, або втрачається.
Yjs — найзрілішої CRDT-бібліотеці для браузера. Інтегрується з ProseMirror, TipTap, CodeMirror, Monaco Editor. Провайдери синхронізації: y-websocket для WebSocket-backend'у, y-webrtc для P2P без сервера. Persistence — y-indexeddb для локального кешу, y-leveldb на сервері.
На практиці: TipTap (ProseMirror-обгортка) + Yjs + y-websocket + Hocuspocus (Node.js WebSocket-сервер для Yjs з PostgreSQL persistence) — стандартний стек для колаборативного текстового редактора. Hocuspocus з коробки дає авторизацію на рівні документа, awareness (хто зараз редагує), persistence через будь-яке сховище.
Проблема, з якою стикаються майже всі: розмір Yjs-документа зростає часом через історію операцій. Потрібна періодична збірка сміття — snapshot документа + очистка старих операцій. Без цього документ, над яким працювали рік, може важити 50MB.
Масштабування WebSocket
Один WebSocket-сервер тримає з'єднання у пам'яті. Якщо у вас два інстанси за load balancer — користувач на інстансі A не отримає повідомлення, адресоване через інстанс B. Стандартне рішення: Redis Pub/Sub як шина між інстансами. Socket.io з коробки підтримує @socket.io/redis-adapter.
Sticky sessions (IP hash на Nginx) — альтернатива, але вона створює нерівномірне розподілення нагрузки та не спасає при падінні інстанса.
Для дуже великих нагрузок (100k+ одночасних з'єднань) — окремий WebSocket gateway (Pushpin, Centrifugo, Ably) перед основним API. Centrifugo написаний на Go, тримає 1M+ з'єднань на одному сервері та інтегрується з будь-яким backend'ом через HTTP API.
Типові проблеми реалізації
Memory leak на сервері. Забули видалити обробник події при закритті з'єднання. На Node.js це видно через process.memoryUsage() — heap зростає ~1MB на годину. EventEmitter попереджає про 10+ слухачів, але не завжди помічають.
Thundering herd при реконнекті. Сервер впав на 30 секунд, піднявся — 10 000 клієнтів пытаются переподключитися одночасно. Exponential backoff з jitter обов'язковий: delay = Math.min(baseDelay * 2^attempt + random(0, 1000), maxDelay).
Немає індикації втрати з'єднання. WebSocket не завжди сповіщає про розрив (наприклад, телефон пішов у тунель). Heartbeat: клієнт відправляє ping кожні 25 секунд, якщо pong не прийшов через 5 секунд — переподключення.
Процес роботи
Починаємо з вибору транспорту під конкретні сценарії використання — іноді в одному проекті потрібні всі три: SSE для системних нотифікацій, WebSocket для чату, WebRTC для відеодзвінків. Проектуємо протокол повідомлень (зазвичай JSON з type та payload, рідше бінарний протокол через MessagePack). Розробляємо з повноцінним тестуванням race conditions — це те, що зазвичай не покривають юнит-тести.
Нагрузкове тестування з k6 + k6/experimental/websockets: моделюємо сценарій 5 000 одночасних з'єднань з реальним паттерном відправки повідомлень.
Графік
Базовий WebSocket-чат або нотифікації поверх існуючого API: 1–3 тижні. Колаборативний редактор з Yjs та persistence: 4–8 тижнів. WebRTC відеодзвінки з записом: 6–12 тижнів, значна частина — інтеграція з медіа-сервером (mediasoup, Janus).







