Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Події, надіслані з сервера, пулінг та довгий пулінг: що це таке та коли їх використовувати». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Polling, long polling та Server-Sent Events (SSE)** - це три патерни доставки серверних оновлень у браузер без ручних запитів з боку клієнта. ```js // Polling: повторний запит кожні N секунд setInterval(() => fetch('/data').then(r => r.json()).then(console.log), 5000); // SSE: сервер надсилає події через одне постійне з'єднання const es = new EventSource('/events'); es.onmessage = e => console.log(e.data); ``` **Ключове:** polling питає постійно незалежно від нових даних (великі накладні витрати); long polling тримає один запит до появи даних; SSE тримає одне з'єднання відкритим і сервер сам пише в нього коли хоче.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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 тримають з'єднання відкритим, це дренує батарею ### Таблиця порівняння | Характеристика | Polling | Long Polling | SSE | |---|---|---|---| | Патерн запитів | Багато коротких запитів | Декілька довгих запитів | Одне постійне з'єднання | | Затримка | Висока (чекає наступного інтервалу) | Низька (спрацьовує на даних) | Найнижча (миттєвий push) | | Навантаження на сервер | Високе (порожні відповіді) | Середнє (утримувані з'єднання) | Низьке (одне з'єднання на клієнта) | | Масштабованість | Погана (обсяг запитів) | Погана (ліміти з'єднань) | Добра (до ~10к з'єднань на сервер) | | Підтримка браузерів | Всі | Всі | 90%+ (IE потребує полізаповнення) | | Складність | Низька | Середня (логіка перепідключення) | Низька (нативний API EventSource) | | Коли використовувати | Рідкісні перевірки (cron-дашборди) | Застарілі браузери без SSE | Live-стрічки, сповіщення, логи | ### Як це працює всередині Для 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. Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.