Skip to main content
backend26. Oktober 202513 Min. Lesezeit

Echtzeit-Daten: WebSockets, SSE und wann Sie sie wirklich brauchen

Echtzeit-Datenmuster verstehen — von WebSockets ueber Server-Sent Events bis hin zu Firebase Realtime Database, und wissen, wann Polling ausreicht.

websocketsrealtimesse
Echtzeit-Daten: WebSockets, SSE und wann Sie sie wirklich brauchen

Die meisten Anwendungen, die behaupten, Echtzeit-Daten zu benoetigen, brauchen tatsaechlich keine Echtzeit-Daten. Sie brauchen Daten, die sich aktuell anfuehlen — innerhalb weniger Sekunden aktualisiert, nicht weniger Millisekunden. Der Unterschied ist wichtig, denn echte Echtzeit-Architekturen fuehren zu Komplexitaet bei Verbindungsverwaltung, State-Synchronisation, Skalierung und Fehlerbehandlung, die voellig unnoetig ist, wenn Ihre Nutzer mit einer Verzoegerung von fuenf Sekunden vollkommen zufrieden waeren.

Ich habe wirklich echte Echtzeit-Features gebaut — Live-Bestellverfolgung fuer eine Restaurant-Plattform, kollaborative Editing-Interfaces, Live-Dashboards — und ich habe auch Features gebaut, bei denen ich anfangs mit WebSockets ueberengineert habe und sie spaeter durch Polling ersetzt habe, das alle zehn Sekunden aktualisiert. Niemand hat die Aenderung bemerkt. Mit dem einfachsten Ansatz zu beginnen und Komplexitaet nur hinzuzufuegen, wenn Nutzer tatsaechlich ein Problem erleben, ist nicht nur pragmatisch — es ist die verantwortungsvolle Engineering-Entscheidung.

Wann Sie wirklich Echtzeit brauchen

Echte Echtzeit ist gerechtfertigt, wenn der Wert von Informationen mit zunehmender Latenz schnell abnimmt.

Chat und Messaging. Nutzer erwarten, dass Nachrichten innerhalb einer Sekunde erscheinen. Eine Verzoegerung von fuenf Sekunden laesst ein Gespraech kaputt wirken. Echtzeit ist hier keine Option.

Kollaboratives Editieren. Gleichzeitiges Bearbeiten im Google-Docs-Stil erfordert Sub-Sekunden-Synchronisation, um widerspruechliche Aenderungen zu vermeiden. Die Komplexitaet geht hier ueber den reinen Transport hinaus — Sie brauchen Operational Transformation oder CRDTs zur Konfliktloesung.

Live-Dashboards mit operativer Auswirkung. Ein Monitoring-Dashboard, auf dem ein Ingenieur waehrend eines Deployments nach Anomalien schaut, braucht Echtzeit-Updates. Ein Business-Analytics-Dashboard, das einmal am Tag angesehen wird, nicht.

Gaming und interaktive Erlebnisse. Multiplayer-Spiele, Live-Auktionen, Echtzeit-Umfragen — alles, bei dem mehrere Nutzer gleichzeitig mit einem gemeinsamen State interagieren.

Finanzhandel. Kurs-Feeds, Orderbuch-Updates, Positionsaenderungen. Millisekunden zaehlen hier, und die Architektur spiegelt das wider.

Wann Sie keine Echtzeit brauchen

Social-Media-Feeds. Twitter und Instagram fuehlen sich wie Echtzeit an, verwenden aber eine Kombination aus Polling beim Fokussieren und Push-Benachrichtigungen. Der Feed aktualisiert sich nicht, waehrend Sie ihn ansehen — Sie ziehen zum Aktualisieren.

E-Commerce-Bestand. "Nur noch 3 uebrig!" muss nicht in Echtzeit aktualisiert werden. Eine Pruefung beim Seitenaufruf und beim Checkout reicht aus.

Benachrichtigungszaehler. Das rote Badge mit "5 neue Benachrichtigungen" kann alle 30 Sekunden abgefragt werden. Nutzer bemerken nicht, wenn eine Benachrichtigung 30 Sekunden nach ihrer Erstellung erscheint.

Inhaltsaktualisierungen. Blogbeitraege, Produkteintraege, Nutzerprofile — alle Daten, die sich selten aendern. Abfrage beim Seitenaufruf oder HTTP-Cache-Header mit Revalidierung verwenden.

Polling: Der unterschaetzte Standard

Polling — periodische HTTP-Anfragen, um nach neuen Daten zu suchen — ist der einfachste Ansatz und funktioniert fuer mehr Anwendungsfaelle als die meisten Entwickler annehmen.

Einfaches Polling

function usePolledData<T>(url: string, intervalMs: number = 5000) {
  const [data, setData] = useState<T | null>(null);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    let active = true;

    const fetchData = async () => {
      try {
        const response = await fetch(url);
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const result = await response.json();
        if (active) setData(result);
      } catch (e) {
        if (active) setError(e as Error);
      }
    };

    fetchData(); // Initial fetch
    const interval = setInterval(fetchData, intervalMs);

    return () => {
      active = false;
      clearInterval(interval);
    };
  }, [url, intervalMs]);

  return { data, error };
}

Intelligentes Polling mit bedingten Anfragen

Bedingte HTTP-Anfragen (If-None-Match mit ETags oder If-Modified-Since) ermoeglichen effizientes Polling. Der Server gibt 304 Not Modified ohne Body zurueck, wenn sich die Daten nicht geaendert haben, und reduziert die Bandbreite fuer unveraenderte Ressourcen auf nahezu null.

async function pollWithETag(url: string): Promise<{ data: any; changed: boolean }> {
  const cachedETag = etagCache.get(url);

  const headers: HeadersInit = {};
  if (cachedETag) {
    headers['If-None-Match'] = cachedETag;
  }

  const response = await fetch(url, { headers });

  if (response.status === 304) {
    return { data: dataCache.get(url), changed: false };
  }

  const etag = response.headers.get('ETag');
  if (etag) etagCache.set(url, etag);

  const data = await response.json();
  dataCache.set(url, data);

  return { data, changed: true };
}

Wann Polling an seine Grenzen stoesst

Polling ist nicht mehr praktikabel, wenn:

  • Sie Sub-Sekunden-Latenz brauchen. Alle 500ms zu pollen ist im Wesentlichen ein DoS-Angriff auf Ihre eigene API.
  • Die Daten sich selten aendern, aber sofort geliefert werden muessen. Alle 5 Sekunden fuer ein Ereignis zu pollen, das einmal pro Stunde eintritt, verschwendet 719 Anfragen pro Stunde.
  • Sie Tausende von Clients haben. Jeder pollende Client ist eine unabhaengige Verbindung. Im grossen Massstab summiert sich der HTTP-Overhead fuer Verbindungsaufbau, Header-Parsing und Authentifizierung.

Server-Sent Events: Das einfachere Echtzeit

Server-Sent Events (SSE) sind die am meisten unterschaetzte Echtzeit-Technologie in der Webentwicklung. SSE bietet einen Einweg-Stream vom Server zum Client ueber eine Standard-HTTP-Verbindung. Der Browser uebernimmt die Wiederverbindung automatisch, und das Protokoll ist trivial einfach.

Server-Implementierung

// Express.js SSE endpoint
app.get('/events/orders/:restaurantId', (req, res) => {
  const { restaurantId } = req.params;

  // SSE headers
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
    'X-Accel-Buffering': 'no', // Disable nginx buffering
  });

  // Send initial data
  const initialOrders = getActiveOrders(restaurantId);
  res.write(`data: ${JSON.stringify(initialOrders)}\n\n`);

  // Subscribe to order changes
  const unsubscribe = orderEmitter.on(`orders:${restaurantId}`, (order) => {
    res.write(`event: orderUpdate\n`);
    res.write(`data: ${JSON.stringify(order)}\n`);
    res.write(`id: ${order.id}-${order.updatedAt}\n\n`);
  });

  // Heartbeat to detect dead connections
  const heartbeat = setInterval(() => {
    res.write(`: heartbeat\n\n`);
  }, 30000);

  // Cleanup on disconnect
  req.on('close', () => {
    clearInterval(heartbeat);
    unsubscribe();
  });
});

Client-Implementierung

function useSSE(url: string) {
  const [data, setData] = useState(null);
  const [connected, setConnected] = useState(false);

  useEffect(() => {
    const eventSource = new EventSource(url);

    eventSource.onopen = () => setConnected(true);

    eventSource.onmessage = (event) => {
      setData(JSON.parse(event.data));
    };

    eventSource.addEventListener('orderUpdate', (event) => {
      const order = JSON.parse(event.data);
      setData(prev => updateOrderInList(prev, order));
    });

    eventSource.onerror = () => {
      setConnected(false);
      // EventSource automatically reconnects
    };

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

  return { data, connected };
}

Warum SSE statt WebSockets

Automatische Wiederverbindung. Die EventSource-Implementierung des Browsers uebernimmt die Wiederverbindung mit exponentiellem Backoff automatisch. Bei WebSockets implementieren Sie das selbst.

Funktioniert durch Proxies und Load Balancer. SSE verwendet Standard-HTTP und funktioniert daher durch nginx, CloudFlare und die meisten Reverse Proxies ohne spezielle Konfiguration. WebSockets erfordern explizite Proxy-Unterstuetzung.

Einfachere Authentifizierung. SSE-Verbindungen uebertragen Cookies und koennen Standard-HTTP-Authentifizierungs-Header verwenden. WebSocket-Authentifizierung erfordert das Senden von Anmeldedaten nach dem Verbindungsaufbau, was ein zusaetzliches Protokoll zum Implementieren ist.

Eingebaute Event-Typen und IDs. Das SSE-Protokoll unterstuetzt benannte Events und Nachrichten-IDs, die es dem Client ermoeglichen, nach einer Wiederverbindung dort fortzufahren, wo er aufgehoert hat.

SSE-Einschraenkungen

  • Nur in eine Richtung. Server zum Client. Wenn Sie bidirektionale Kommunikation brauchen, kann SSE das nicht leisten. Sie koennen SSE mit regulaeren HTTP-POST-Anfragen fuer Client-zu-Server-Nachrichten kombinieren, was fuer viele Anwendungsfaelle gut funktioniert.
  • Browser-Verbindungslimits. Browser begrenzen die Anzahl gleichzeitiger HTTP-Verbindungen zu einer einzelnen Domain (typischerweise sechs). Jede SSE-Verbindung zaehlt zu diesem Limit. HTTP/2-Multiplexing mildert dies ab, aber es ist gut, sich dessen bewusst zu sein.
  • Keine Binaerdaten. SSE ist nur textbasiert. Wenn Sie Binaerdaten streamen muessen (Audio, Video, Dateien), verwenden Sie WebSockets oder einen separaten Mechanismus.

WebSockets: Vollduplex-Kommunikation

WebSockets bieten Vollduplex-Kommunikation — sowohl Client als auch Server koennen jederzeit Nachrichten senden, ohne den Overhead von HTTP-Request/Response-Zyklen. Das ist notwendig, wenn Sie bidirektionale Kommunikation mit niedriger Latenz brauchen.

Server-Implementierung mit ws

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

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

// Connection management
const rooms = new Map<string, Set<WebSocket>>();

wss.on('connection', (ws, req) => {
  // Authenticate the connection
  const token = new URL(req.url!, `http://${req.headers.host}`).searchParams.get('token');
  const user = verifyToken(token);

  if (!user) {
    ws.close(4001, 'Unauthorized');
    return;
  }

  // Attach user data
  (ws as any).userId = user.id;

  ws.on('message', (raw) => {
    try {
      const message = JSON.parse(raw.toString());
      handleMessage(ws, user, message);
    } catch (e) {
      ws.send(JSON.stringify({ error: 'Invalid message format' }));
    }
  });

  ws.on('close', () => {
    // Remove from all rooms
    rooms.forEach((clients, roomId) => {
      clients.delete(ws);
      if (clients.size === 0) rooms.delete(roomId);
    });
  });

  // Heartbeat
  (ws as any).isAlive = true;
  ws.on('pong', () => { (ws as any).isAlive = true; });
});

// Dead connection detection
const heartbeatInterval = setInterval(() => {
  wss.clients.forEach((ws) => {
    if (!(ws as any).isAlive) {
      ws.terminate();
      return;
    }
    (ws as any).isAlive = false;
    ws.ping();
  });
}, 30000);

function handleMessage(ws: WebSocket, user: any, message: any) {
  switch (message.type) {
    case 'join_room':
      joinRoom(ws, message.roomId);
      break;
    case 'leave_room':
      leaveRoom(ws, message.roomId);
      break;
    case 'broadcast':
      broadcastToRoom(message.roomId, {
        type: 'message',
        userId: user.id,
        content: message.content,
        timestamp: Date.now(),
      }, ws);
      break;
  }
}

function joinRoom(ws: WebSocket, roomId: string) {
  if (!rooms.has(roomId)) rooms.set(roomId, new Set());
  rooms.get(roomId)!.add(ws);
}

function broadcastToRoom(roomId: string, data: any, exclude?: WebSocket) {
  const clients = rooms.get(roomId);
  if (!clients) return;

  const payload = JSON.stringify(data);
  clients.forEach((client) => {
    if (client !== exclude && client.readyState === WebSocket.OPEN) {
      client.send(payload);
    }
  });
}

server.listen(8080);

Client-Implementierung mit Wiederverbindung

class WebSocketClient {
  private ws: WebSocket | null = null;
  private url: string;
  private reconnectAttempts = 0;
  private maxReconnectAttempts = 10;
  private handlers = new Map<string, Set<(data: any) => void>>();

  constructor(url: string) {
    this.url = url;
  }

  connect() {
    this.ws = new WebSocket(this.url);

    this.ws.onopen = () => {
      this.reconnectAttempts = 0;
      this.emit('connected', null);
    };

    this.ws.onmessage = (event) => {
      try {
        const message = JSON.parse(event.data);
        this.emit(message.type, message);
      } catch (e) {
        console.error('Failed to parse WebSocket message:', e);
      }
    };

    this.ws.onclose = (event) => {
      this.emit('disconnected', { code: event.code, reason: event.reason });

      if (event.code !== 4001 && this.reconnectAttempts < this.maxReconnectAttempts) {
        this.scheduleReconnect();
      }
    };

    this.ws.onerror = () => {
      // Error is followed by close event, handle reconnection there
    };
  }

  private scheduleReconnect() {
    const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
    this.reconnectAttempts++;

    setTimeout(() => {
      this.emit('reconnecting', { attempt: this.reconnectAttempts });
      this.connect();
    }, delay);
  }

  send(type: string, data: any) {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify({ type, ...data }));
    }
  }

  on(event: string, handler: (data: any) => void) {
    if (!this.handlers.has(event)) this.handlers.set(event, new Set());
    this.handlers.get(event)!.add(handler);
    return () => this.handlers.get(event)?.delete(handler);
  }

  private emit(event: string, data: any) {
    this.handlers.get(event)?.forEach((handler) => handler(data));
  }

  disconnect() {
    this.maxReconnectAttempts = 0; // Prevent reconnection
    this.ws?.close();
  }
}

Wiederverbindungsstrategie

Die Wiederverbindungslogik verdient Aufmerksamkeit, weil sie direkt die Nutzererfahrung und die Serverlast beeinflusst.

Exponentielles Backoff verhindert Thundering-Herd-Probleme. Wenn Ihr Server neu startet und 10.000 Clients gleichzeitig wiederverbinden, stuerzt der Server erneut ab. Durch Hinzufuegen von Jitter und exponentiellen Verzoegerungen verteilen sich die Verbindungen ueber die Zeit:

function getReconnectDelay(attempt: number): number {
  const baseDelay = 1000;
  const maxDelay = 30000;
  const exponentialDelay = baseDelay * Math.pow(2, attempt);
  const jitter = Math.random() * 1000; // Random jitter up to 1 second
  return Math.min(exponentialDelay + jitter, maxDelay);
}

Verbindungsstatus-UI ist wichtig. Nutzer muessen wissen, wann sie getrennt sind und wann die App versucht, sich wieder zu verbinden. Ein kleines Banner mit "Verbindung wird wiederhergestellt..." ist besser als stillschweigend veraltete Daten anzuzeigen.

Socket.IO: Komfort vs. Kontrolle

Socket.IO ist die beliebteste WebSocket-Bibliothek und bietet erheblichen Komfort: automatische Wiederverbindung, Raumverwaltung, Bestaetigungen und Fallback auf Polling, wenn WebSockets nicht verfuegbar sind. Es fuegt auch erheblichen Overhead hinzu und fuehrt eine Abstraktionsschicht ein, die Probleme verbergen kann.

Wann Socket.IO verwenden

  • Schnelles Prototyping. Die API von Socket.IO ist einfacher als rohe WebSockets, und die eingebauten Features sparen Entwicklungszeit.
  • Wenn Sie Polling-Fallback brauchen. Einige Unternehmensnetzwerke blockieren WebSocket-Verbindungen. Socket.IO faellt automatisch auf HTTP Long Polling zurueck.
  • Raum- und Namespace-Verwaltung. Wenn Ihr Echtzeit-Feature natuerlich auf Raeume abbildet (Chat-Raeume, Spiel-Lobbys, kollaborative Dokumente), sparen die Raum-Abstraktionen von Socket.IO Boilerplate-Code.

Wann Socket.IO vermeiden

  • Hochperformante Anwendungen. Das Protokoll von Socket.IO fuegt jeder Nachricht Overhead hinzu (Framing, Encoding, Metadaten). Fuer Anwendungen mit hohem Durchsatz sind rohe WebSockets messbar schneller.
  • Wenn Sie Interoperabilitaet brauchen. Socket.IO ist kein Standard-WebSocket — ein regulaerer WebSocket-Client kann sich nicht mit einem Socket.IO-Server verbinden. Wenn Ihre Clients Nicht-JavaScript-Umgebungen umfassen (Mobile Apps, IoT-Geraete), benoetigt Socket.IO eine Client-Bibliothek fuer jede Plattform.
  • Wenn Sie verstehen wollen, was passiert. Socket.IO abstrahiert die Verbindungsverwaltung, was bequem ist, bis etwas schiefgeht. Das Debuggen eines Socket.IO-Verbindungsproblems bedeutet oft, die Abstraktionsschicht ueber dem eigentlichen Problem zu verstehen.

Firebase Realtime Database und Firestore

Firebase bietet Echtzeit-Faehigkeiten ohne die Verwaltung einer WebSocket-Infrastruktur. Das ist sein Haupt-Wertversprechen — Sie tauschen Kontrolle gegen Komfort.

Firebase Realtime Database

import { getDatabase, ref, onValue, set } from 'firebase/database';

const db = getDatabase();

// Listen for real-time updates
const ordersRef = ref(db, `restaurants/${restaurantId}/orders`);
onValue(ordersRef, (snapshot) => {
  const orders = snapshot.val();
  updateUI(orders);
});

// Write data (triggers listeners on all connected clients)
await set(ref(db, `restaurants/${restaurantId}/orders/${orderId}`), {
  status: 'preparing',
  updatedAt: Date.now(),
});

Die Realtime Database ist ein JSON-Baum. Jeder Schreibvorgang auf einen Knoten propagiert sofort an jeden Client, der diesen Knoten oder einen uebergeordneten Knoten beobachtet. Das ist maechtig, aber gefaehrlich — ein Listener auf einem hoeherliegenden Knoten erhaelt Updates fuer jede Aenderung im gesamten Unterbaum.

Firestore Echtzeit-Listener

import { getFirestore, collection, onSnapshot, query, where } from 'firebase/firestore';

const db = getFirestore();

// Listen for active orders
const q = query(
  collection(db, 'orders'),
  where('restaurantId', '==', restaurantId),
  where('status', 'in', ['pending', 'preparing', 'ready'])
);

const unsubscribe = onSnapshot(q, (snapshot) => {
  snapshot.docChanges().forEach((change) => {
    if (change.type === 'added') addOrder(change.doc.data());
    if (change.type === 'modified') updateOrder(change.doc.data());
    if (change.type === 'removed') removeOrder(change.doc.id);
  });
});

Die Echtzeit-Listener von Firestore sind granularer als die der Realtime Database — Sie koennen spezifische Queries beobachten, nicht nur Pfade. Die docChanges()-Methode sagt Ihnen genau, was sich geaendert hat, was effiziente UI-Updates ermoeglicht.

Supabase Realtime

Supabase bietet Echtzeit-Faehigkeiten auf Basis von PostgreSQL, was Ihnen die Abfrage-Power von SQL mit Echtzeit-Updates gibt. Es nutzt die Replikationsfunktionen von PostgreSQL (logische Replikation und das WAL), um Aenderungen zu erkennen und sie ueber WebSocket-Channels zu verbreiten.

import { createClient } from '@supabase/supabase-js';

const supabase = createClient(url, key);

// Listen for changes to the orders table
const channel = supabase
  .channel('orders')
  .on(
    'postgres_changes',
    {
      event: '*',
      schema: 'public',
      table: 'orders',
      filter: `restaurant_id=eq.${restaurantId}`,
    },
    (payload) => {
      console.log('Change type:', payload.eventType);
      console.log('New data:', payload.new);
      console.log('Old data:', payload.old);
    }
  )
  .subscribe();

Der Vorteil gegenueber Firebase ist klar: Ihre Daten liegen in PostgreSQL mit vollen SQL-Abfragefaehigkeiten, ACID-Transaktionen und Standard-Tooling. Die Echtzeit-Schicht ist eine Ergaenzung, kein Ersatz fuer Ihre Query-Engine. Der Kompromiss ist, dass Supabase Realtime im grossen Massstab weniger erprobt ist als Firebase und die Echtzeit-Performance vom Replikationsdurchsatz von PostgreSQL abhaengt.

WebSocket-Verbindungen skalieren

Ein einzelner Server kann Zehntausende von WebSocket-Verbindungen verarbeiten, aber die Skalierung ueber einen Server hinaus fuehrt zu einem Koordinationsproblem: Eine Nachricht, die auf Server A veroeffentlicht wird, muss Clients erreichen, die mit Server B verbunden sind.

Das Pub/Sub-Muster

Redis Pub/Sub ist die Standardloesung:

import { createClient } from 'redis';

const publisher = createClient();
const subscriber = createClient();

await publisher.connect();
await subscriber.connect();

// When a message comes in on any server, publish to Redis
function publishToRoom(roomId: string, message: any) {
  publisher.publish(`room:${roomId}`, JSON.stringify(message));
}

// Each server subscribes and forwards to its local connections
await subscriber.subscribe(`room:${roomId}`, (message) => {
  const data = JSON.parse(message);
  broadcastToLocalClients(roomId, data);
});

Dieses Muster erlaubt es Ihnen, mehrere WebSocket-Server hinter einem Load Balancer zu betreiben. Jeder Server verwaltet seine eigenen Verbindungen, und Redis koordiniert die serveruebergreifende Kommunikation.

Sticky Sessions

WebSocket-Verbindungen sind zustandsbehaftet — einmal aufgebaut, muessen sie auf dem gleichen Server bleiben. Load Balancer brauchen Sticky Sessions (auch Session Affinity genannt), um die WebSocket-Verbindung eines Clients an den gleichen Server weiterzuleiten, der das initiale HTTP-Upgrade verarbeitet hat.

Mit nginx:

upstream websocket_servers {
    ip_hash;  # Sticky sessions based on client IP
    server ws1.example.com:8080;
    server ws2.example.com:8080;
}

server {
    location /ws {
        proxy_pass http://websocket_servers;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_read_timeout 86400;  # 24 hours
    }
}

Echtzeit vs. Nahezu-Echtzeit

Es gibt ein Spektrum zwischen "beim Neuladen der Seite nach Updates suchen" und "jede Aenderung innerhalb von Millisekunden ausliefern". Die meisten Features liegen irgendwo dazwischen.

Ansatz Latenz Komplexitaet Am besten fuer
Seite neu laden Nutzer-initiiert Keine Statische Inhalte, seltene Aenderungen
Polling (30s) Bis zu 30 Sekunden Niedrig Benachrichtigungszaehler, Dashboard-Updates
Polling (5s) Bis zu 5 Sekunden Niedrig Aktivitaets-Feeds, Bestellstatus
Long Polling Sub-Sekunde Mittel Aeltere Systeme, Proxy-feindliche Umgebungen
SSE Sub-Sekunde Mittel Live-Feeds, Benachrichtigungen, Einweg-Streams
WebSockets Sub-Sekunde Hoch Chat, Kollaboration, bidirektionale Echtzeit
WebRTC Millisekunden Sehr hoch Video/Audio, Peer-to-Peer

Der richtige Ansatz ist der einfachste, der Ihre Latenz-Anforderung erfuellt. Beginnen Sie mit Polling. Wenn sich Nutzer ueber Veraltung beschweren, steigen Sie auf SSE um. Wenn Sie bidirektionale Kommunikation brauchen, verwenden Sie WebSockets. Jeder Schritt auf der Komplexitaetsleiter sollte durch einen konkreten Nutzerbedarf gerechtfertigt sein, nicht durch eine hypothetische zukuenftige Anforderung.

Praktische Empfehlungen

Nach dem Bau von Echtzeit-Features in mehreren Projekten sind dies die Muster, auf die ich immer wieder zurueckkomme:

Standardmaessig SSE fuer Einweg-Echtzeit. Die meisten Echtzeit-Features sind Server-zu-Client: Bestell-Updates, Benachrichtigungs-Streams, Live-Daten-Feeds. SSE handhabt all das mit weniger Komplexitaet als WebSockets.

WebSockets nur fuer bidirektionale Anforderungen verwenden. Chat, kollaboratives Editieren, Multiplayer-Interaktionen — diese brauchen wirklich WebSockets. Wenn Ihr Client nur Daten empfaengt, ist SSE einfacher.

Immer Wiederverbindung mit Backoff implementieren. Verbindungen brechen ab. Netzwerke wechseln. Server starten neu. Ihr Client muss damit elegant umgehen, und Ihr Server muss einen Wiederverbindungssturm ueberstehen.

Diffs senden, nicht den gesamten State. Anstatt bei jeder Aenderung einer Bestellung die gesamte Bestellliste zu senden, senden Sie nur die geaenderte Bestellung. Der Client wendet den Diff auf seinen lokalen State an. Das reduziert Bandbreite und Verarbeitung auf beiden Seiten.

Einen Fallback haben. Wenn Ihre WebSocket-Verbindung fehlschlaegt und sich nicht wieder verbinden kann, sollte die Anwendung trotzdem funktional sein. Ein Hinweis "Zuletzt aktualisiert vor 30 Sekunden" mit manueller Aktualisierung ist besser als ein leerer Bildschirm.

Verbindungsanzahl in Produktion ueberwachen. WebSocket-Verbindungen verbrauchen Serverressourcen (Speicher, Datei-Deskriptoren). Ein ploetzlicher Anstieg der Verbindungen — zum Beispiel durch einen Client-Bug, der schnelle Wiederverbindungen verursacht — kann Ihren Server zum Absturz bringen. Alarmieren Sie bei Anomalien der Verbindungsanzahl.

Das Ziel ist nicht, die ausgefeilteste Echtzeit-Architektur zu haben. Das Ziel ist, Daten schnell genug an die Nutzer zu liefern, damit sich die Anwendung responsiv anfuehlt, mit der geringstmoeglichen Infrastruktur-Komplexitaet, die dieses Gefuehl erreicht.

DU

Danil Ulmashev

Full Stack Developer

Interesse an einer Zusammenarbeit?