Представьте: пользователь открывает dashboard с динамическими данными. Вы реализовали стандартный клиентский опрос (polling). Каждые 5 секунд фронтенд дергает бэкенд: "Есть обновления? Нет? Ладно...". Сотни одновременных пользователей — и ваш сервер тонет в паразитных запросах. Большинство ответов — 304 Not Modified. Трафик, нагрузка, задержки. Знакомая картина?
Почему поллинг терпит поражение
Типичный setInterval
подход — инвалидация данных:
// Наивная реализация поллинга (не делайте так)
const fetchData = async () => {
const response = await fetch('/api/updates');
// Обработка данных
};
setInterval(fetchData, 5000);
Проблемы глубже технических:
- Экономические: До 70% запросов возвращают неизмененные данные — пустая трата ресурсов
- Латентность: Задержка актуализации ≈ половине интервала + время обработки
- Масштабирование: Экспоненциальный рост нагрузки при увеличении пользователей
- Буферные сбои: Кратковременные сетевые проблемы убивают актуальность данных
Server-Sent Events: реактивная альтернатива для данных в режиме чтения
SSE — однонаправленный канал сервер → клиент через HTTP. Идеально для панелей мониторинга, уведомлений, логов.
Бэкенд на Node.js (Express):
import express from 'express';
const app = express();
const clients = new Map();
app.get('/updates/:userId', (req, res) => {
const userId = req.params.userId;
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// Сохранить подключение
clients.set(userId, res);
req.on('close', () => clients.delete(userId));
});
// Механизм рассылки при изменениях (например, из Kafka-консьюмера)
const publishUpdate = (userId, data) => {
const client = clients.get(userId);
if (client) {
client.write(`data: ${JSON.stringify(data)}\n\n`); // Обязательно \n\n в конце
}
};
Фронтенд:
const eventSource = new EventSource('/updates/12345');
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
// Рендеринг актуальных данных
};
eventSource.onerror = () => {
// Автореконнект - встроенное поведение
};
Критические особенности SSE:
- Автовосстановление при разрывах
- Стандартные HTTP-порты (80/443), проходят через большинство корпоративных фаерволов
- Ограничение: однонаправленная связь (сервер → клиент)
WebSockets: полнодуплексная синхронная связь
Когда нужны двусторонние коммуникации (чаты, коллаборативные редакторы, игры) — WebSockets рулят.
Node.js-бэкенд с WS:
import WebSocket from 'ws';
const wss = new WebSocket.Server({ port: 8080 });
const clients = new Set();
wss.on('connection', (ws) => {
clients.add(ws);
ws.on('message', (data) => {
// Обработка входящей команды от клиента
});
ws.on('close', () => clients.delete(ws));
});
// Широковещательная рассылка
const broadcast = (message) => {
clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(message));
}
});
};
Клиентская часть:
const socket = new WebSocket('wss://api.example.com');
socket.addEventListener('open', () => {
socket.send(JSON.stringify({ type: 'AUTH', token: '...' }));
});
socket.addEventListener('message', (event) => {
const message = JSON.parse(event.data);
// Обновляем UI
});
// Отправка действий пользователя
const sendAlignmentUpdate = (position) => {
socket.send(JSON.stringify({ type: 'ALIGN', pos: position }));
};
Нюансы WebSockets:
- Собственный протокол (
ws://
/wss://
), обходит HTTP-инфраструктуру - Двунаправленное обновление в real-time
- Тонкости управления состоянием: проверка соединений, буферизация при переподключении
- Лучше сочетать с Redis Pub/Sub при горизонтальном масштабировании
Выбор технологии: где что применимо
Критерий | Поллинг | SSE | WebSocket |
---|---|---|---|
Направление данных | Client → Server | Server → Client | Двунаправленное |
Совместимость | Все браузеры | Все кроме IE | Все современные |
Трафик | Высокий | Низкий | Минимальный |
Латенция | Высокая | Низкая | Очень низкая |
Сложность реализации | Низкая | Средняя | Высокая |
Сценарии | Простые запросы | Панели данных | Интерфейсы онлайн-визарда или чаты |
Паттерны балансировки реального мира
При росте нагрузки до тысяч подключений:
- Sticky-сессии для WebSocket: Один бэкенд → один клиент
- Redis Pub/Sub для широковещательных SSE:
// Рассылка событий через Redis всем нодам кластера
redisSubscriber.on('message', (channel, message) => {
executeOnAllNodes(`for (client of clients) client.send(${message})`);
});
- HTTP/2 Push+Streams как гибридная альтернатива SSE с мультиплексированием (но меньшая консистентность)
Обработка сбоев: проектируйте для ненадежности
- Повтор соединения: экспоненциальное затухание в обоих протоколах
- Критичные данные: идентификаторы последовательности для восстановления состояния
- Деградация: fallback к поллингу при блокировке SSE/WS корпфаерволом
// Прогрессивное восстановление на фронтенде
function connectRealtime() {
const ws = new WebSocket(endpoint);
ws.onclose = () => {
setTimeout(() => {
// Сначала пробуем вновь открыть WebSocket
connectRealtime();
}, 1000);
// Если разрывы повторяются – деградируем в SSE/поллинг
};
}
Реальные показатели
В типичной системе IoT-monitoring после перехода с поллинга (5s) на SSE:
- Трафик сервера ↓ на 82%
- CPU-нагрузка ↓ на 63%
- 99-й перцентиль задержки обновления: с 4.2s → 0.3s
Инженерный выбор всегда компромисс. SSE предлагает элегантную простоту для server-push данных. WebSockets требуют инвестиций для двусторонних систем. Поллинг остаётся валидным для редких обновлений при жестких требованиях к инфраструктуре.
Ключевой принцип: проектируйте протокол обмена данными как чистую модель потока событий. События производятся -> трансформируются -> потребляются. Инструментарий вторичен. И если архитектура спроектирована верно — количество 304 Not Modified бочча останется читателям этого абзаца.