Skip to main content

Події, надіслані з сервера, пулінг та довгий пулінг: що це таке та коли їх використовувати

Polling, long polling та Server-Sent Events (SSE) - це три техніки на основі HTTP для доставки серверних оновлень у браузер. Кожна по-своєму балансує між кількістю запитів, навантаженням на з'єднання і затримкою.

Теорія

TL;DR

  • Polling питає сервер "є щось нове?" за фіксованим таймером, навіть якщо нічого не змінилось
  • Long polling тримає запит відкритим, поки сервер не матиме даних, потім одразу перезапускає його
  • SSE тримає одне з'єднання відкритим постійно - сервер сам надсилає події коли хоче
  • Аналогія: polling - дитина, яка перевіряє поштову скриньку кожні 5 хвилин; long polling стоїть біля скриньки і чекає; SSE - листоноша, який телефонує тобі напряму по одній відкритій лінії
  • Правило вибору: рідкісні перевірки → polling; мала затримка без WebSocket → long polling; часті односторонні події → SSE

Швидкий приклад

js
// Polling: запит кожні 3с, навіть якщо даних немає setInterval(() => fetch('/data').then(r => r.json()).then(console.log), 3000); // Long polling: один запит чекає на дані або таймаут async function longPoll() { const res = await fetch('/long-data'); // сервер тримає ~30с console.log(await res.json()); // виводить раз на цикл longPoll(); // одразу перезапускаємо } longPoll(); // SSE: одне з'єднання, браузер перепідключається автоматично const es = new EventSource('/events'); es.onmessage = e => console.log(e.data); // виводить кожну подію

Polling виводить результат незалежно від наявності нових даних. Long polling - раз за цикл. SSE - одразу, як тільки сервер щось надіслав.

Ключова різниця

Polling відкриває новий HTTP-запит на кожному інтервалі, незалежно від того чи змінились дані. Long polling відкриває один запит, який сервер тримає відкритим до появи даних або таймауту, і клієнт одразу надсилає наступний. SSE відкриває одне HTTP-з'єднання з Content-Type: text/event-stream і тримає його постійно. Сервер записує рядки вигляду data: ...\n\n у сокет коли хоче, без повторних хендшейків.

Коли що використовувати

  • Оновлення дашборду кожні 30+ секунд: polling підходить, реалізація проста
  • Чат без підтримки WebSocket або SSE: long polling забезпечує малу затримку
  • Live-сповіщення, котирування акцій, стрімінг логів: SSE
  • Двостороння комунікація (ігри, спільні редактори): жоден з трьох, потрібен WebSocket
  • Мобільні клієнти: long polling і SSE тримають з'єднання відкритим, це дренує батарею

Таблиця порівняння

ХарактеристикаPollingLong PollingSSE
Патерн запитівБагато коротких запитівДекілька довгих запитівОдне постійне з'єднання
ЗатримкаВисока (чекає наступного інтервалу)Низька (спрацьовує на даних)Найнижча (миттєвий push)
Навантаження на серверВисоке (порожні відповіді)Середнє (утримувані з'єднання)Низьке (одне з'єднання на клієнта)
МасштабованістьПогана (обсяг запитів)Погана (ліміти з'єднань)Добра (до ~10к з'єднань на сервер)
Підтримка браузерівВсіВсі90%+ (IE потребує полізаповнення)
СкладністьНизькаСередня (логіка перепідключення)Низька (нативний API EventSource)
Коли використовуватиРідкісні перевірки (cron-дашборди)Застарілі браузери без SSELive-стрічки, сповіщення, логи

Як це працює всередині

Для polling і long polling браузер використовує fetch або XMLHttpRequest. У Node.js/Express long polling реалізується так: об'єкт res зберігається в пам'яті, а res.end(data) викликається лише коли з'явились дані або спрацював setTimeout.

SSE використовує chunked transfer encoding з HTTP/1.1. API EventSource у браузері читає рядки зі стріму і шукає префікс data: з двома переносами рядка (\n\n). Якщо з'єднання обривається, EventSource автоматично перепідключається через 3 секунди. У Node потрібно викликати res.flushHeaders() (Node 18+), інакше відповідь буферизується і клієнт нічого не отримує до закриття з'єднання.

З HTTP/2 ліміт у 6 з'єднань на домен фактично знімається: потоки мультиплексуються через одне TCP-з'єднання.

Типові помилки

1. Немає обробника помилок у EventSource

js
// Неправильно: з'єднання падає без сигналу const es = new EventSource('/events'); es.onmessage = e => console.log(e.data); // Правильно: явно обробляємо розрив es.onerror = () => console.log('З\'єднання SSE втрачено, браузер повторить спробу...');

Браузер повторює підключення 3 рази, потім зупиняється. Без onerror UI просто зависає без жодного сигналу.

2. Немає таймауту в long poll запиті

js
// Неправильно: зависає назавжди при краші сервера const res = await fetch('/long-data'); // Правильно: AbortController з 35-секундним таймаутом const controller = new AbortController(); setTimeout(() => controller.abort(), 35000); const res = await fetch('/long-data', { signal: controller.signal });

3. Занадто агресивний інтервал polling

js
// Неправильно: 1с = 86 400 запитів на день з одного клієнта setInterval(() => fetch('/stocks'), 1000); // Правильно: починати від 5с, додати exponential backoff

4. Забули викликати flushHeaders у Node 18+

js
// Без цього Node буферизує відповідь і клієнт нічого не бачить res.flushHeaders();

5. SSE для двосторонньої передачі даних

Надсилати дані від клієнта через query params SSE - це не те, для чого протокол призначений, це руйнує потік. При потребі в обох напрямках використовуй WebSocket.

Де зустрічається в реальних проектах

  • GitHub: SSE для live-сповіщень у веб-інтерфейсі
  • Twitter/X: SSE для стрімінгу стрічки твітів у веб-застосунку
  • Socket.io v4: автоматично переходить на long polling, якщо WebSocket недоступний
  • React Query та SWR: polling для фонової інвалідації кешу (за замовчуванням 5 хвилин)
  • Firebase Realtime Database: long polling як запасний варіант для старих браузерів

Питання на співбесіді

Q: Як SSE відрізняється від WebSocket на рівні протоколу?
A: SSE працює через звичайний HTTP/1.1 з chunked transfer encoding і є одностороннім (сервер до клієнта). WebSocket виконує HTTP upgrade handshake, потім переключається на сире TCP-з'єднання, яке є двостороннім без HTTP-накладних витрат на кожне повідомлення.

Q: Який ліміт з'єднань SSE у HTTP/1.1?
A: Приблизно 6 на домен, як і для будь-яких HTTP/1.1 з'єднань. З HTTP/2 потоки мультиплексуються через одне TCP-з'єднання, тому це обмеження практично зникає.

Q: Чому б не замінити polling на SSE скрізь?
A: SSE є одностороннім. Деякі файрволи та проксі закривають довготривалі з'єднання. SSE не підтримує бінарні дані нативно. Для простих рідкісних перевірок на кшталт оновлення дашборду кожні 30 секунд polling простіший і працює всюди.

Q: Яких затримок очікувати від кожного підходу?
A: Затримка polling дорівнює приблизно половині інтервалу (в середньому чекаєш пів циклу). Long polling додає ~100 мс накладних витрат. SSE доставляє події менш ніж за 50 мс після встановлення з'єднання.

Q: Продакшн-сценарій: 10 000 користувачів на long polling піднімають CPU Node.js до 100%. Як виправити без переходу на WebSocket?
A: Long polling з 10 000 користувачів - це 10 000 активних таймерів в event loop Node, він просто захлинається. Рішення: Redis pub/sub. Замість того щоб кожен запит тримав свій таймер, він підписується на Redis-канал. Один publish повідомляє всі очікуючі відповіді одночасно. Перехід на SSE також допомагає - одне постійне з'єднання на клієнта масштабується краще ніж 10 000 повторних циклів запит-відповідь.

Приклади

Polling: базовий таймерний запит

js
// Перевіряє оновлення кожні 5 секунд // _t запобігає кешуванню відповіді function startPolling(url, interval = 5000) { setInterval(async () => { const res = await fetch(`${url}?_t=${Date.now()}`); const data = await res.json(); console.log('Свіжі дані:', data); }, interval); } startPolling('/api/dashboard');

Просто написати, але кожен запит іде на сервер незалежно від змін. При 1000 клієнтах і інтервалі 5 секунд - це 12 000 запитів на хвилину, більшість з яких повертають порожній результат.

SSE: Express сервер і React компонент

Подібно до того, як GitHub доставляє live-сповіщення.

Сервер (Express.js):

js
app.get('/events', (req, res) => { res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' }); res.flushHeaders(); // обов'язково для Node 18+ const interval = setInterval(() => { res.write(`data: ${JSON.stringify({ msg: 'Нове сповіщення', ts: Date.now() })}\n\n`); }, 5000); // Очищаємо при відключенні клієнта req.on('close', () => clearInterval(interval)); });

React клієнт:

js
function Notifications() { const [msgs, setMsgs] = useState([]); useEffect(() => { const es = new EventSource('/events'); es.onmessage = e => setMsgs(m => [...m, JSON.parse(e.data)]); es.onerror = () => console.log('З\'єднання втрачено, перепідключення...'); return () => es.close(); // очищення при розмонтуванні }, []); return <ul>{msgs.map((m, i) => <li key={i}>{m.msg}</li>)}</ul>; }

Список оновлюється в реальному часі без перезавантаження сторінки. req.on('close') легко забути - без нього при відключенні клієнта інтервал продовжує працювати і з'їдає пам'ять.

Long polling з таймаутом і повторними спробами

Продакшн-код має витримувати мережеві збої та перезапуск сервера.

js
async function robustLongPoll(url, maxRetries = 5) { for (let attempt = 0; attempt < maxRetries; attempt++) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 35000); try { const res = await fetch(url, { signal: controller.signal }); const data = await res.json(); clearTimeout(timeout); console.log('Отримано:', data); return data; } catch (err) { clearTimeout(timeout); console.log(`Спроба ${attempt + 1} невдала: ${err.message}`); // Чекаємо довше між кожною повторною спробою await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt))); } } console.log('Перехід на polling'); } // Перезапускаємо після кожної успішної відповіді async function keepPolling(url) { while (true) { await robustLongPoll(url); } }

35-секундний таймаут навмисно довший за 30-секундне утримання на сервері. Нормальний таймаут сервера завершується коректно, а реальний збій перериває AbortController і вмикає exponential backoff.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?