О насБлогКонтакты
Разработка18 апреля 2026 г. 5 мин 6

WebSockets vs SSE vs Long Polling: выбор технологии realtime для вашего приложения

AunimedaAunimeda
📋 Содержание

WebSockets vs SSE vs Long Polling: выбор технологии realtime для вашего приложения

Когда нужно показывать данные в реальном времени — чат, статус заказа, онлайн-счётчики, уведомления — встаёт вопрос: какую технологию выбрать? Каждая имеет свои сценарии, ограничения и компромиссы.


Краткое сравнение

Критерий WebSocket SSE Long Polling
Направление Двунаправленный Сервер → клиент Сервер → клиент
Протокол ws:// wss:// HTTP HTTP
Автоматическое переподключение Нет (вручную) Да (браузер) Зависит от реализации
Поддержка браузеров 98%+ 98%+ (нет IE) 100%
Поддержка прокси/балансировщиков Требует настройки Нативно Нативно
Нагрузка на сервер Низкая (persistent conn) Низкая Высокая
Сложность реализации Высокая Низкая Средняя

Long Polling — простейший вариант

Клиент делает HTTP-запрос. Сервер держит соединение открытым до появления данных (или таймаута), затем отвечает. Клиент немедленно делает следующий запрос.

// Сервер (Express)
const pendingClients = new Map<string, Response>();

app.get('/api/notifications/poll', authenticate, async (req, res) => {
  const userId = req.user.id;
  
  res.setTimeout(30_000); // 30 секунд таймаут
  
  // Ставим в очередь ожидающих клиентов
  pendingClients.set(userId, res);
  
  req.on('close', () => {
    pendingClients.delete(userId);
  });
});

// Когда приходит уведомление — отправляем ожидающему клиенту
async function sendNotification(userId: string, notification: Notification) {
  const client = pendingClients.get(userId);
  if (client) {
    client.json(notification);
    pendingClients.delete(userId);
  }
  // Также сохраняем в БД для клиентов не онлайн
  await db.notification.create({ data: { userId, ...notification } });
}
// Клиент
async function startLongPolling(userId: string) {
  while (true) {
    try {
      const response = await fetch('/api/notifications/poll', {
        signal: AbortSignal.timeout(35_000),
      });
      
      if (response.ok) {
        const notification = await response.json();
        displayNotification(notification);
      }
    } catch {
      // Таймаут или ошибка — ждём и повторяем
      await new Promise(resolve => setTimeout(resolve, 1000));
    }
  }
}

Когда использовать Long Polling:

  • Нечастые обновления (раз в минуту и реже)
  • Простота важнее оптимальности
  • Нет возможности настроить WebSocket на прокси
  • Нужна максимальная совместимость

Server-Sent Events (SSE)

Браузер открывает HTTP-соединение и получает события потоком. Нативная поддержка в браузере включает автоматическое переподключение.

// Сервер
const sseClients = new Map<string, Response[]>();

app.get('/api/events', authenticate, (req, res) => {
  const userId = req.user.id;
  
  // Заголовки SSE
  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
  res.flushHeaders();
  
  // Регистрируем клиента
  const clients = sseClients.get(userId) ?? [];
  clients.push(res);
  sseClients.set(userId, clients);
  
  // Heartbeat — предотвращает таймаут прокси
  const heartbeat = setInterval(() => {
    res.write(':heartbeat\n\n');
  }, 25_000);
  
  req.on('close', () => {
    clearInterval(heartbeat);
    const remaining = (sseClients.get(userId) ?? []).filter(c => c !== res);
    if (remaining.length === 0) {
      sseClients.delete(userId);
    } else {
      sseClients.set(userId, remaining);
    }
  });
});

// Отправка событий клиенту
function sendEvent(userId: string, eventType: string, data: object) {
  const clients = sseClients.get(userId) ?? [];
  const message = `event: ${eventType}\ndata: ${JSON.stringify(data)}\n\n`;
  clients.forEach(client => client.write(message));
}

// Использование:
sendEvent(userId, 'order_status', { orderId: '123', status: 'shipped' });
sendEvent(userId, 'notification', { text: 'Ваш заказ отправлен!' });
// Клиент — браузер делает всё автоматически
const eventSource = new EventSource('/api/events', { withCredentials: true });

eventSource.addEventListener('order_status', (e) => {
  const data = JSON.parse(e.data);
  updateOrderStatus(data.orderId, data.status);
});

eventSource.addEventListener('notification', (e) => {
  const data = JSON.parse(e.data);
  showNotification(data.text);
});

// Автоматическое переподключение — браузер делает это сам
eventSource.onerror = (err) => {
  console.log('SSE ошибка, браузер переподключится автоматически');
};

Когда использовать SSE:

  • Обновления только от сервера к клиенту (уведомления, статусы)
  • Нужно простое автоматическое переподключение
  • Работа за стандартными HTTP балансировщиками
  • Нет необходимости в двунаправленном обмене

WebSocket — двунаправленный канал

WebSocket создаёт постоянное соединение для обмена данными в обе стороны. Идеален для чатов, совместного редактирования, online-игр.

Нативный WebSocket (без библиотек)

import { WebSocketServer, WebSocket } from 'ws';
import http from 'http';

const server = http.createServer(app);
const wss = new WebSocketServer({ server });

interface AuthenticatedWS extends WebSocket {
  userId?: string;
  isAlive?: boolean;
}

const connectedUsers = new Map<string, Set<AuthenticatedWS>>();

wss.on('connection', async (ws: AuthenticatedWS, req) => {
  // Аутентификация через token в URL или cookie
  const token = new URL(req.url!, 'ws://localhost').searchParams.get('token');
  const userId = await verifyToken(token);
  
  if (!userId) {
    ws.close(4001, 'Unauthorized');
    return;
  }
  
  ws.userId = userId;
  ws.isAlive = true;
  
  const userConns = connectedUsers.get(userId) ?? new Set();
  userConns.add(ws);
  connectedUsers.set(userId, userConns);

  ws.on('message', async (data) => {
    try {
      const message = JSON.parse(data.toString());
      await handleMessage(ws, message);
    } catch (err) {
      ws.send(JSON.stringify({ type: 'error', message: 'Invalid message format' }));
    }
  });

  ws.on('pong', () => { ws.isAlive = true; });

  ws.on('close', () => {
    const conns = connectedUsers.get(userId!);
    conns?.delete(ws);
    if (conns?.size === 0) connectedUsers.delete(userId!);
  });
});

// Ping/pong для обнаружения мёртвых соединений
setInterval(() => {
  wss.clients.forEach((ws: AuthenticatedWS) => {
    if (!ws.isAlive) return ws.terminate();
    ws.isAlive = false;
    ws.ping();
  });
}, 30_000);

// Отправка пользователю (на все его вкладки/устройства)
function sendToUser(userId: string, data: object) {
  const connections = connectedUsers.get(userId);
  connections?.forEach(ws => {
    if (ws.readyState === WebSocket.OPEN) {
      ws.send(JSON.stringify(data));
    }
  });
}

Socket.io — когда нужна надёжность

Socket.io добавляет: автоматические переподключения, fallback на polling, rooms, namespaces:

import { Server } from 'socket.io';

const io = new Server(server, {
  cors: { origin: process.env.FRONTEND_URL, credentials: true },
  transports: ['websocket', 'polling'], // polling как fallback
});

// Middleware для аутентификации
io.use(async (socket, next) => {
  try {
    const token = socket.handshake.auth.token;
    socket.data.userId = await verifyToken(token);
    next();
  } catch {
    next(new Error('Unauthorized'));
  }
});

io.on('connection', (socket) => {
  const { userId } = socket.data;
  
  // Пользователь присоединяется к персональной комнате
  socket.join(`user:${userId}`);
  
  socket.on('chat:message', async (data: { roomId: string; text: string }) => {
    const message = await db.message.create({
      data: { userId, roomId: data.roomId, text: data.text },
    });
    // Отправляем всем в комнате
    io.to(`room:${data.roomId}`).emit('chat:message', message);
  });

  socket.on('join:room', (roomId: string) => {
    socket.join(`room:${roomId}`);
  });
});

// Уведомление конкретному пользователю (работает даже на нескольких серверах с Redis)
function notifyUser(userId: string, event: string, data: object) {
  io.to(`user:${userId}`).emit(event, data);
}

Масштабирование: Redis Pub/Sub

На нескольких серверах WebSocket-соединения распределены по разным узлам. Redis Pub/Sub синхронизирует их:

import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';

const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();

await Promise.all([pubClient.connect(), subClient.connect()]);

io.adapter(createAdapter(pubClient, subClient));

// Теперь io.to('room:xxx').emit() работает на всех серверах

Выбор для конкретных задач

Задача Рекомендация
Push-уведомления SSE
Статус заказа/доставки SSE
Чат WebSocket
Совместное редактирование WebSocket
Live-счётчики на сайте SSE
Онлайн-игры WebSocket (нативный)
Редкие обновления (<1/мин) Long Polling

Aunimeda реализует realtime-функциональность для бизнес-приложений. WhatsApp.

Смотрите также: Telegram Bot FSM и middleware, Node.js vs Bun runtime

Читайте также

Supabase vs Firebase: что выбрать для стартапа в Бишкекеaunimeda
Разработка

Supabase vs Firebase: что выбрать для стартапа в Бишкеке

Supabase — open-source с PostgreSQL, самохостинг возможен. Firebase — зрелый Google BaaS. PocketBase — один бинарник для MVP. Сравниваем модели данных, цены, realtime и когда каждый вариант оправдан для бишкекских стартапов.

Чистая архитектура в Node.js: как организовать код чтобы не стать заложникомaunimeda
Разработка

Чистая архитектура в Node.js: как организовать код чтобы не стать заложником

Большинство Node.js проектов начинаются с routes + controllers + models и через полгода превращаются в спагетти. Показываем как правильно разделить бизнес-логику от инфраструктуры — use cases, repository pattern, тестируемый код без базы данных.

Вайб-кодинг в Бишкеке: как местные разработчики используют ИИ для ускорения работы в 2026aunimeda
Разработка

Вайб-кодинг в Бишкеке: как местные разработчики используют ИИ для ускорения работы в 2026

Разработчики в Бишкеке используют Cursor AI, Claude Code и ChatGPT не как поиск по Stack Overflow, а как полноценного соавтора кода. Что изменилось, кто выигрывает и как не потерять квалификацию.

Нужна IT-разработка для вашего бизнеса?

Разрабатываем сайты, мобильные приложения и AI-решения для бизнеса в Кыргызстане. Бесплатная консультация.

Получить консультацию Все статьи