Реализация Live Updates (обновления без перезагрузки) на сайте
Обновления в реальном времени без перезагрузки страницы — это не обязательно WebSocket. Выбор технологии зависит от направленности данных, частоты обновлений и допустимой сложности инфраструктуры.
Три подхода и когда применять каждый
Server-Sent Events (SSE) — однонаправленный поток с сервера, обычный HTTP. Идеален для уведомлений, лент активности, прогресса задач. Встроена автоматическая переподключение браузером.
WebSocket — двунаправленный канал. Нужен, когда клиент тоже отправляет данные в реальном времени (чат, игры, совместное редактирование).
Polling / Long Polling — HTTP-запросы с интервалом или ожидающие ответа. Простейший вариант, подходит для редких обновлений (раз в 30–60 секунд).
| Технология | Направление | Инфраструктура | Когда |
|---|---|---|---|
| SSE | Сервер → Клиент | Любой HTTP-сервер | Уведомления, фиды, статусы |
| WebSocket | Двунаправленный | WS-сервер | Чат, игры, коллаборация |
| Polling | Клиент → Сервер | Любой | Редкие обновления, простота |
| Long Polling | Клиент ↔ Сервер | Любой | Fallback для SSE |
Server-Sent Events: реализация
SSE работает через обычный HTTP-ответ с Content-Type: text/event-stream. Соединение остаётся открытым, сервер пушит события:
// Node.js / Express
app.get('/api/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no'); // Важно для nginx
const userId = req.user.id;
// Отправка начального состояния
res.write(`data: ${JSON.stringify({ type: 'init', unread: 5 })}\n\n`);
// Подписка на события
const unsubscribe = eventBus.subscribe(userId, (event) => {
res.write(`event: ${event.type}\n`);
res.write(`data: ${JSON.stringify(event.payload)}\n`);
res.write(`id: ${event.id}\n\n`); // для Last-Event-ID
});
// Keepalive каждые 30 секунд
const heartbeat = setInterval(() => {
res.write(': heartbeat\n\n');
}, 30000);
req.on('close', () => {
clearInterval(heartbeat);
unsubscribe();
});
});
Клиент:
const evtSource = new EventSource('/api/events', {
withCredentials: true,
});
// Слушаем именованные события
evtSource.addEventListener('notification', (e) => {
const data = JSON.parse(e.data);
showNotification(data);
});
evtSource.addEventListener('order-status', (e) => {
updateOrderStatus(JSON.parse(e.data));
});
// Общий обработчик
evtSource.onmessage = (e) => {
console.log('Default event:', e.data);
};
// Браузер автоматически переподключается
evtSource.onerror = (e) => {
console.log('SSE error, will reconnect...');
};
EventSource автоматически переподключается при обрыве с отправкой Last-Event-ID — сервер может отправить пропущенные события.
WebSocket с переподключением
Нативный WebSocket не переподключается сам. Нужна обёртка:
class ReconnectingWebSocket {
constructor(url, protocols) {
this.url = url;
this.protocols = protocols;
this.reconnectDelay = 1000;
this.maxDelay = 30000;
this.listeners = new Map();
this.connect();
}
connect() {
this.ws = new WebSocket(this.url, this.protocols);
this.ws.onopen = () => {
this.reconnectDelay = 1000;
this.emit('open');
};
this.ws.onmessage = (e) => this.emit('message', JSON.parse(e.data));
this.ws.onclose = () => {
this.emit('close');
setTimeout(() => this.connect(), this.reconnectDelay);
this.reconnectDelay = Math.min(this.reconnectDelay * 1.5, this.maxDelay);
};
}
send(data) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
}
}
on(event, cb) {
if (!this.listeners.has(event)) this.listeners.set(event, []);
this.listeners.get(event).push(cb);
}
emit(event, data) {
this.listeners.get(event)?.forEach(cb => cb(data));
}
}
Или использовать готовое: reconnecting-websocket, socket.io (со встроенным fallback на polling).
Обновление UI без мигания
Грубая замена innerHTML при каждом обновлении создаёт визуальные артефакты. Два подхода:
Morphdom — DOM-diff без Virtual DOM:
import morphdom from 'morphdom';
ws.on('message', ({ type, html }) => {
if (type === 'update-block') {
const el = document.getElementById('notifications');
morphdom(el, `<div id="notifications">${html}</div>`, {
onBeforeElUpdated: (fromEl, toEl) => {
// Не обновляем элементы в процессе анимации
if (fromEl.classList.contains('animating')) return false;
return true;
}
});
}
});
React / Vue state updates — если фронтенд на React, просто обновляем состояние:
ws.on('message', (data) => {
switch (data.type) {
case 'new-order':
setOrders(prev => [data.order, ...prev]);
break;
case 'order-updated':
setOrders(prev => prev.map(o => o.id === data.order.id ? data.order : o));
break;
case 'notification':
setNotifications(prev => [data.notification, ...prev.slice(0, 49)]);
break;
}
});
Broadcasting через Redis Pub/Sub
При нескольких инстансах сервера — Redis Pub/Sub для рассылки событий всем подключённым клиентам:
// publisher.js
const redis = require('redis');
const publisher = redis.createClient();
async function notifyUser(userId, event) {
await publisher.publish(
`user:${userId}`,
JSON.stringify(event)
);
}
// subscriber.js (в том же процессе, что держит SSE/WS соединения)
const subscriber = redis.createClient();
await subscriber.subscribe(`user:${userId}`, (message) => {
const event = JSON.parse(message);
sseConnections.get(userId)?.forEach(res => {
res.write(`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`);
});
});
Оптимизация: batch updates и debounce
При высокочастотных обновлениях (цены, метрики) не отправляем каждое изменение отдельно:
// Сервер: буферизация событий
class UpdateBatcher {
constructor(flushInterval = 100) {
this.queue = new Map(); // userId -> события
setInterval(() => this.flush(), flushInterval);
}
queue(userId, event) {
if (!this.queue.has(userId)) this.queue.set(userId, []);
this.queue.get(userId).push(event);
}
flush() {
this.queue.forEach((events, userId) => {
if (events.length) {
sendBatch(userId, events);
this.queue.set(userId, []);
}
});
}
}
Сроки
- SSE-уведомления (новые заказы, сообщения) — 1–2 дня
- WebSocket с переподключением и React-интеграцией — 2–3 дня
- Broadcasting через Redis Pub/Sub для нескольких серверов — плюс 1–2 дня
- Полноценная real-time лента (активность, уведомления, счётчики) — 4–6 дней







