Volver al Blog
SoftwareWebDesarrollo

Sincronización en Tiempo Real con WebSockets y Server-Sent Events (SSE) en Next.js 15

Brayan Developer
6 min de lectura
Sincronización en Tiempo Real con WebSockets y Server-Sent Events (SSE) en Next.js 15
Domina la implementación de dashboards interactivos y notificaciones en vivo utilizando Server-Sent Events (SSE), WebSockets y Redis Pub/Sub en Next.js 15 y Node.js.

La interactividad en tiempo real se ha convertido en un requisito indispensable para aplicaciones web modernas: tableros de control financiero, monitoreo IoT, chats en vivo y notificaciones de pedidos. En Next.js 15, elegir la arquitectura correcta entre Server-Sent Events (SSE) y WebSockets es determinante para el rendimiento y la escalabilidad de la infraestructura. En esta guía técnica analizamos cuándo utilizar cada protocolo y cómo implementarlos paso a paso.

Portada

Comparativa: WebSockets vs Server-Sent Events (SSE)#

Antes de escribir código, es crucial comprender las diferencias fundamentales entre ambas tecnologías:

Característica Server-Sent Events (SSE) WebSockets (WS)
Direccionalidad Unidireccional (Servidor → Cliente) Bidireccional full-duplex (Servidor ↔ Cliente)
Protocolo HTTP estándar (HTTP/1.1 chunked, HTTP/2 multiplexado) Protocolo TCP independiente (ws:// / wss://)
Reconexión Automática Nativa en el navegador vía EventSource Manual o dependiente de librerías (Socket.io)
Soporte Proxy / Firewalls Excelente (funciona sobre puertos 80/443 sin cambios) Requiere soporte de Upgrade header en proxies
Caso de Uso Ideal Notificaciones, feeds de noticias, tickers bursátiles, IA streaming Chats colaborativos, juegos multijugador, pizarras compartidas

Arquitectura de Tiempo Real con Next.js y Redis Pub/Sub#

Cuando escalamos a múltiples instancias de servidor (servidores stateless o clusters en Docker/Kubernetes), necesitamos un bus de mensajes centralizado para que un evento originado en el Servidor A llegue a los clientes conectados al Servidor B.

+-------------------------------------------------------------------------------+
|                            CLIENTES (Navegadores Web)                         |
|   +-----------------------+              +--------------------------------+   |
|   | Dashboard SSE Client  |              | Chat WebSockets Client         |   |
|   +-----------------------+              +--------------------------------+   |
+-------------------------------------------------------------------------------+
                 | (EventSource GET /api/sse)               | (WSS Connection)
                 v                                          v
+-----------------------------------+      +------------------------------------+
|  NEXT.js 15 APP ROUTER INSTANCE 1 |      |   NODE.JS WS GATEWAY INSTANCE 2    |
|   (Route Handlers Streaming API)  |      |      (ws / Socket.io Server)       |
+-----------------------------------+      +------------------------------------+
                 \                                          /
                  \                                        /
                   v                                      v
+-------------------------------------------------------------------------------+
|                      BUS DE MENSAJES DISTRIBUIDO (REDIS)                      |
|                  Redis Pub/Sub (Channels: "alerts", "live-orders")            |
+-------------------------------------------------------------------------------+

Implementación 1: Server-Sent Events (SSE) con Next.js 15 App Router#

Next.js 15 permite crear endpoints SSE ultra-eficientes utilizando la API estándar de ReadableStream en Route Handlers (app/api/events/route.ts).

Endpoint SSE en Next.js (src/app/api/events/route.ts)#

import { NextRequest } from 'next/server';

export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';

export async function GET(req: NextRequest) {
  const encoder = new TextEncoder();

  const stream = new ReadableStream({
    start(controller) {
      // Enviar evento de bienvenida
      controller.enqueue(
        encoder.encode(`event: connected
data: ${JSON.stringify({ message: "Conectado al stream SSE" })}

`)
      );

      // Intervalo de latido / actualización (Heartbeat cada 5s)
      const intervalId = setInterval(() => {
        const payload = {
          timestamp: new Date().toISOString(),
          activeUsers: Math.floor(Math.random() * 200) + 50,
          cpuUsage: (Math.random() * 30 + 10).toFixed(1) + "%",
        };

        controller.enqueue(
          encoder.encode(`event: metrics
data: ${JSON.stringify(payload)}

`)
        );
      }, 5000);

      // Limpieza al cerrar la conexión
      req.signal.addEventListener('abort', () => {
        clearInterval(intervalId);
        controller.close();
      });
    },
  });

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache, no-transform',
      'Connection': 'keep-alive',
      'X-Accel-Buffering': 'no', // Evita buffering en NGINX
    },
  });
}

Consumo en Cliente React con EventSource (DashboardMetrics.tsx)#

'use client';

import { useEffect, useState } from 'react';

interface Metrics {
  timestamp: string;
  activeUsers: number;
  cpuUsage: string;
}

export function DashboardMetrics() {
  const [metrics, setMetrics] = useState<Metrics | null>(null);
  const [status, setStatus] = useState<'connecting' | 'connected' | 'disconnected'>('connecting');

  useEffect(() => {
    const eventSource = new EventSource('/api/events');

    eventSource.addEventListener('connected', () => {
      setStatus('connected');
    });

    eventSource.addEventListener('metrics', (event) => {
      const data: Metrics = JSON.parse(event.data);
      setMetrics(data);
    });

    eventSource.onerror = () => {
      setStatus('disconnected');
      // El navegador reintentará automáticamente
    };

    return () => {
      eventSource.close();
    };
  }, []);

  return (
    <div className="p-6 bg-slate-900 border border-slate-800 rounded-xl text-white">
      <div className="flex items-center justify-between mb-4">
        <h3 className="text-lg font-bold">Telemetría en Vivo</h3>
        <span className={`inline-block w-3 h-3 rounded-full ${status === 'connected' ? 'bg-emerald-500 animate-pulse' : 'bg-rose-500'}`} />
      </div>

      {metrics ? (
        <div className="grid grid-cols-2 gap-4">
          <div className="p-4 bg-slate-800 rounded-lg">
            <p className="text-sm text-slate-400">Usuarios Activos</p>
            <p className="text-2xl font-semibold text-emerald-400">{metrics.activeUsers}</p>
          </div>
          <div className="p-4 bg-slate-800 rounded-lg">
            <p className="text-sm text-slate-400">Uso de CPU</p>
            <p className="text-2xl font-semibold text-cyan-400">{metrics.cpuUsage}</p>
          </div>
        </div>
      ) : (
        <p className="text-slate-400">Esperando primer paquete de métricas...</p>
      )}
    </div>
  );
}

Implementación 2: Servidor WebSockets Dedicado con Redis Adapter#

Para interacciones bidireccionales de baja latencia (como chats colaborativos o edición simultánea), desacoplar el servidor de WebSockets en una instancia Node.js independiente con Socket.io y @socket.io/redis-adapter asegura escalabilidad horizontal ilimitada.

// server/websocket-gateway.ts
import http from 'http';
import { Server } from 'socket.io';
import { createClient } from 'redis';
import { createAdapter } from '@socket.io/redis-adapter';

const server = http.createServer();
const io = new Server(server, {
  cors: {
    origin: ["https://brayan.es", "http://localhost:3000"],
    methods: ["GET", "POST"]
  }
});

async function bootstrap() {
  const pubClient = createClient({ url: process.env.REDIS_URL || "redis://localhost:6379" });
  const subClient = pubClient.duplicate();

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

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

  io.on('connection', (socket) => {
    console.log(`[WS] Cliente conectado: ${socket.id}`);

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

    socket.on('send_message', (data: { roomId: string; message: string; author: string }) => {
      // Re-emitir a todos los usuarios en la sala
      io.to(data.roomId).emit('new_message', {
        ...data,
        createdAt: new Date().toISOString()
      });
    });

    socket.on('disconnect', () => {
      console.log(`[WS] Cliente desconectado: ${socket.id}`);
    });
  });

  const PORT = process.env.PORT || 4000;
  server.listen(PORT, () => {
    console.log(`🚀 Servidor WebSockets escuchando en el puerto ${PORT}`);
  });
}

bootstrap();

Buenas Prácticas de Rendimiento y Seguridad#

  1. Evitar sobrecargar el hilo principal de Node.js: Enviar únicamente deltas de datos (cambios) en lugar del estado completo en cada evento.
  2. Heartbeat y Detección de Caídas: Enviar pings regulares para detectar conexiones fantasma (ghost sockets) y liberar memoria en el servidor.
  3. Autenticación con JWT en el Handshake: Validar el token de sesión durante la conexión inicial y asociar el socket al userId verificado.
  4. Protección DDoS y Rate Limiting: Limitar el número máximo de mensajes por segundo por IP utilizando Redis Token Bucket.

Conclusión#

La combinación de Server-Sent Events para flujos unidireccionales (notificaciones y métricas) y WebSockets con Redis para aplicaciones colaborativas bidireccionales ofrece el equilibrio perfecto entre simplicidad, rendimiento y costo de infraestructura.

¿Necesitas diseñar una arquitectura en tiempo real escalable para tu plataforma web o SaaS? Revisa nuestros Servicios de Desarrollo de Software o agenda una llamada técnica con Brayan Developer.

Etiquetas
Next.jsWebSocketsServer-Sent EventsNode.jsRedis Pub/SubTiempo Real
Compartir:XLinkedInWhatsApp

¿Te gustaría profundizar en estos temas?

Aprende sobre desarrollo de software, apps a medida, automatizaciones con N8N, Next.js y Cloud con casos reales.

Hablemos