Server-Sent Events (EventSource)

Server-Sent Events (EventSource) #

Server-Sent Events (SSE) — способ получать от сервера поток текстовых событий по одному HTTP-соединению. Клиентский API — класс EventSource: браузер сам переподключается и парсит формат text/event-stream. Подходит для ленты активности, счётчиков на дашборде и статусов сборки — когда клиенту нужно только слушать, без частых ответов серверу.


Базовое подключение #

const feed = new EventSource('/api/dashboard/activity')

feed.onmessage = (event) => {
  const row = JSON.parse(event.data)
  appendActivityRow(row.user, row.action)
}

feed.onerror = () => {
  console.warn('Поток активности прерван, EventSource переподключится')
}

Первый аргумент — URL (обычно same-origin). В отличие от fetch, соединение остаётся открытым: сервер периодически дописывает строки в тело ответа.


Формат события на проводе #

Сервер отправляет текстовые блоки, разделённые пустой строкой:

data: {"trackId":"trk-12","listeners":384}

data: {"trackId":"trk-12","listeners":385}

Поле data может повторяться — строки склеиваются через \n. Необязательные поля:

ПолеНазначение
eventИмя типа события (не message)
idИдентификатор для заголовка Last-Event-ID при reconnect
retryИнтервал переподключения в миллисекундах

Именованные события #

Если сервер шлёт event: order-status, слушайте отдельный тип:

const orders = new EventSource('/api/checkout/stream')

orders.addEventListener('order-status', (event) => {
  const { orderId, step } = JSON.parse(event.data)
  updateCheckoutStep(orderId, step)
})

orders.addEventListener('ping', () => {
  // keep-alive без полезной нагрузки
})

События без поля event попадают в обработчик onmessage.


Закрытие и состояние #

const feed = new EventSource('/api/metrics/live')

// readyState: CONNECTING (0), OPEN (1), CLOSED (2)
document.getElementById('pause-feed').addEventListener('click', () => {
  feed.close()
})

После close() переподключения не будет. При ошибке сети EventSource сам восстанавливает соединение (если не вызван close()), отправляя Last-Event-ID, если сервер ранее прислал id.


SSE vs WebSocket vs polling #

EventSourceWebSocketPolling
НаправлениеСервер → клиентДвустороннееЗапрос–ответ
ПротоколHTTPОтдельный WSHTTP
ПереподключениеВстроеноВручнуюКаждый tick
Бинарные данныеНет (только текст)ДаЗависит от ответа
// polling — лишняя нагрузка каждые N секунд
setInterval(async () => {
  const res = await fetch('/api/cart/count')
  const { count } = await res.json()
  setBadge(count)
}, 5000)

// SSE — сервер пушит при изменении
const cart = new EventSource('/api/cart/stream')
cart.onmessage = (e) => setBadge(JSON.parse(e.data).count)

Для чата или совместного редактирования, где клиент часто пишет в канал, выберите WebSocket. Для ленты логов и метрик SSE часто проще в эксплуатации.


Ограничения #

  1. Только GET — тело запроса не отправить; авторизацию передают cookie (same-origin) или query-токен по политике сервера.
  2. Текст — бинарные payload нужно кодировать (Base64) или использовать WebSocket.
  3. Лимит соединений — в HTTP/1.1 браузер ограничивает число параллельных запросов к одному хосту; для многих потоков предпочтителен HTTP/2.
  4. CORS — cross-origin SSE возможен, если сервер выставляет заголовки доступа; детали пересекаются с темой CORS.

Итог #

  • EventSource открывает долгоживущий HTTP-поток text/event-stream.
  • События по умолчанию — onmessage; именованные типы — через addEventListener('event-name').
  • Браузер переподключается автоматически; close() останавливает поток.
  • SSE — односторонний push; для двустороннего обмена используйте WebSocket.
  • Предпочитайте SSE polling-у, когда обновления редкие и инициатор — сервер.

См. также #

Часто спрашивают

Почему fetch «падает» на чужом домене?
Браузер блокирует чтение ответа без CORS-заголовков с сервера. На фронте это выглядит как сетевая ошибка TypeError. Подробнее
Когда нужен WebSocket, а когда SSE?
WebSocket — двусторонний канал; SSE — односторонний поток событий с сервера проще для подписок «сервер → клиент». Подробнее