Засновник питає: «Нам потрібні real-time-оновлення».
Половину часу — не потрібні. Іншу половину WebSocket — це overkill для того, що їм насправді треба. Є середина: Server-Sent Events і короткий polling. Будь-який з них відвантажується за пів дня, масштабується без спеціалізованої інфраструктури і покриває більшість того, що команди називають «real-time».
Цей пост — про ту середину. Що вона вміє, що ні, і коли WebSocket — справді правильна відповідь.
Що насправді означає «real-time»
«Real-time» — це слово, яким люди позначають різні речі. Варто закріпити перед вибором інфри:
Швидка eventual consistency. Користувач зробив дію; інший користувач має побачити результат за кілька секунд. Індикатори набору, бейджі «нове повідомлення», тости «хтось ще прокоментував». Більшість продуктів називає це real-time. Це не воно технічно, але досвід нормальний, якщо оновлення приходять за 2-5 секунд.
Живі оновлення без рефрешу. Те саме, але з сильнішими очікуваннями затримки: 200ms-1s. Біржові тікери, лічильники дашборда, що мають відчуватись живими, колаборативні курсори.
Двостороння low-latency. Обидві сторони мають надсилати оновлення одна одній за десятки мілісекунд. Мультиплеєрні ігри, voice і video, колаборативні курсори у Figma. Справді потребує WebSocket або WebRTC.
Якщо ваша потреба першого типу — WebSocket не потрібний. Якщо другого — може й не потрібний. Якщо третього — потрібний.
Варіант polling
Найтупіша можлива відповідь часто працює: опитуйте сервер кожні кілька секунд.
useEffect(() => {
const interval = setInterval(() => {
fetch('/api/messages').then(...);
}, 3000);
return () => clearInterval(interval);
}, []);Три секунди затримки на індикатор «нове повідомлення» — нормально для більшості продуктів. Користувачі не помічають. Ціна — один HTTP-запит кожні три секунди на активного користувача.
Для продукту з 1000 одночасних користувачів — це 333 запити/секунду. Next.js-застосунок на Vercel справляється без поту. Postgres з одним індексом на потрібній колонці справляється без поту. Сумарна ціна: нуль інженерної складності, можливо $20/місяць трафіку.
Коли polling правильний:
- Оновлення, що не мають бути миттєвими.
- Кількість користувачів — до, скажімо, 10000 одночасно активних.
- Ви не платите за per-request інфру (а Vercel і більшість платформ — ні, нижче їх лімітів тарифу).
- Хочете тупу, дебагабельну, легку для роздумів імплементацію.
Паттерн може бути розумнішим за наївний polling: збільшуйте інтервал, коли таб неактивний; зменшуйте, коли користувач активний; back off після помилок. Але базовий випадок — шість рядків коду.
Варіант Server-Sent Events
SSE — це HTTP, односторонній, з сервера до клієнта. Сервер тримає з'єднання відкритим і пушить дані, коли вони є. Клієнт автоматично перепідключається при розриві.
// Сервер (Next.js Route Handler)
export async function GET() {
const stream = new ReadableStream({
start(controller) {
const interval = setInterval(() => {
controller.enqueue(`data: ${JSON.stringify({ now: Date.now() })}\n\n`);
}, 1000);
},
});
return new Response(stream, {
headers: { 'Content-Type': 'text/event-stream' },
});
}// Клієнт
useEffect(() => {
const source = new EventSource('/api/events');
source.onmessage = (event) => { ... };
return () => source.close();
}, []);Що отримуєте:
- Sub-секунда push-затримки.
- Автоматичне перепідключення.
- Одне з'єднання на клієнта (на відміну від одного запиту на інтервал з polling).
- Працює через HTTP/2 і HTTP/3 без сюрпризів.
- Жодної нової інфри. Це просто HTTP.
Чого не отримуєте:
- Client-to-server push. SSE — однонаправлений. Клієнт усе ще використовує звичайні HTTP-запити, щоб надсилати дані.
- Браузерний ліміт одночасних з'єднань на origin. З HTTP/1.1 — 6. З HTTP/2 і HTTP/3 — багато. Використовуйте HTTP/2.
SSE — правильна відповідь, коли:
- Треба sub-секундна push-затримка для нотифікацій, живих лічильників, оновлень дашборда.
- Комунікація переважно server-to-client (так і є для 80% «real-time»-фіч).
- Хочете масштабуватись поза per-request ціною polling без операційного навантаження WebSocket.
Підводний камінь на serverless-платформах: long-lived з'єднання коштують грошей. Vercel тарифікує за час виконання, і SSE-з'єднання, що лишається відкритим годину — це година виконання. Конкретно для Vercel роутайте SSE-ендпоінти на long-lived рантайм (Edge або виділений worker), або винесіть SSE-роботу на сервіс, спроєктований для неї (Cloudflare Workers, Fly, виділений Node-процес).
Коли WebSocket справді правильний
Випадки, коли SSE і polling не вистачає:
1. Двостороння low-latency. І клієнт, і сервер мають надсилати оновлення один одному за менше ніж 100ms. Приклади: стан мультиплеєрної гри, voice/video-сигналінг, колаборативне редагування з позиціями курсорів.
2. Високочастотні оновлення з клієнта. Клієнт надсилає більше кількох подій за секунду. Polling-based «надсилання» плюс SSE-based «отримання» працює для низькочастотних клієнтських подій, але відчувається дивно вище 5-10 подій за секунду.
3. Кастомні бінарні протоколи. Ви надсилаєте не лише JSON. WebSocket підтримує binary frames; SSE — ні.
Для цих випадків WebSocket — правильний. Ціна:
- Стан з'єднання, яким треба керувати. WebSocket-з'єднання рвуться. Перепідключення — на вашій стороні.
- Інфраструктура. Більшість serverless-платформ не підтримує persistent WebSocket-з'єднання з коробки. Ви будете використовувати managed-сервіс (Liveblocks, Ably, Pusher, PartyKit) або тримати власний Node-процес на long-running сервері.
- Операції. Масштабування WebSocket потребує sticky session або fan-out сервісу. Обидва додають складності.
Якщо ваша фіча справді мультиплеєрно-ігрового рівня real-time — цей overhead виправданий. Якщо це «показати нотифікацію при новому повідомленні» — ні.
Дерево рішення
Стартуйте з polling. Підіймайтесь вище лише з причини.
- Чи можу опитувати кожні 3-5 секунд? Якщо так — зупиніться тут. Затримка нормальна. Інфра — жодної.
- Чи треба sub-секунда push-затримки? Якщо так — використайте SSE. Усе ще жодної спеціалізованої інфри.
- Чи треба client-to-server push на високій частоті? Якщо так — WebSocket. Тепер потрібна managed-інфра або long-running процес.
- Чи треба binary frames або sub-100ms двосторонньо? Якщо так — WebSocket обов'язковий. Використайте managed-сервіс, якщо немає причин для self-host.
Більшість проєктів, які ми відвантажуємо, зупиняються на кроці 1 або 2. Крок 3+ існує, але це виняток, не правило.
Опрацьований приклад
Ми нещодавно відвантажили B2B SaaS з «real-time»-вимогами. Команда спочатку просила WebSocket. Ми штовхнули назад і запитали, що конкретно має бути real-time. Список:
- Нові тікети саппорта з'являються у адмін-черзі.
- Лічильник бейджа нотифікацій оновлюється, коли приходять нові тікети.
- Індикатор «інший адмін зараз дивиться цей тікет».
Для тікетів і бейджів SSE був overkill. Ми використали 5-секундний polling. Нерозрізнюваний від real-time на людському масштабі B2B-саппорту.
Для «зараз дивиться цей тікет» ми використали SSE. Одне з'єднання на адміна, сервер пушить presence-оновлення щоразу, коли хтось відкриває чи закриває тікет. Список «користувачі, що зараз дивляться» оновлюється за менше ніж секунду.
Жодних WebSocket. Жодної нової інфри. Усе працює на існуючому стеку Next.js + Postgres. Ми оцінюємо, що зекономили три тижні роботи і постійну операційну складність. Команда відтоді відвантажила ще дві «real-time»-фічі на тому ж паттерні.
Коли б ви прийшли до нас
Якщо вам кажуть «нам потрібен real-time» і команда тягнеться до WebSocket — варто другої думки. Половину часу відповідь — polling. Третину — SSE. Останню шосту — справжній WebSocket, і ми побудуємо його правильно, коли це відповідь.
Подивіться, як ми працюємо з веб-застосунками. Ми обираємо інфру, що пасує реальній проблемі, не ту, що звучить найвразливіше у статус-апдейті.