Menu
Nazad na Blog
1 min read
Performance

Real-Time Dashboards mit WebSockets: Live-Daten ohne Polling

Aufbau von Echtzeit-Dashboards mit WebSockets. Server-Push, Live-Updates und Performance-Optimierung für datenintensive Anwendungen.

Real-Time DashboardWebSocketLive DataServer PushDashboard DevelopmentData Visualization
Real-Time Dashboards mit WebSockets: Live-Daten ohne Polling

Real-Time Dashboards mit WebSockets: Live-Daten ohne Polling

Meta-Description: Aufbau von Echtzeit-Dashboards mit WebSockets. Server-Push, Live-Updates und Performance-Optimierung für datenintensive Anwendungen.

Keywords: Real-Time Dashboard, WebSocket, Live Data, Server Push, Dashboard Development, Data Visualization, Live Updates


Einführung

Polling ist tot. Moderne Dashboards nutzen Server-Push via WebSockets für Echtzeit-Updates ohne die Last von ständigen HTTP-Requests.


Polling vs. WebSocket

┌─────────────────────────────────────────────────────────────┐
│                  POLLING VS. WEBSOCKET                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Polling (Alt):                                             │
│  Client: "Neue Daten?" → Server: "Nein"                    │
│  Client: "Neue Daten?" → Server: "Nein"                    │
│  Client: "Neue Daten?" → Server: "Nein"                    │
│  Client: "Neue Daten?" → Server: "Ja! Hier."               │
│  └── 4 Requests, 3 unnötig                                 │
│                                                             │
│  WebSocket (Neu):                                           │
│  Client ←─────────────→ Server (persistente Verbindung)    │
│  Server: "Neue Daten!" → Client                            │
│  └── 1 Push, sofort                                        │
│                                                             │
│  Vorteile WebSocket:                                        │
│  ✓ Niedrigere Latenz (ms statt s)                          │
│  ✓ Weniger Server-Last                                     │
│  ✓ Weniger Bandbreite                                      │
│  ✓ Echte Real-Time-Updates                                 │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Server Architecture

// src/dashboard-server.ts
import { WebSocketServer, WebSocket } from 'ws';
import { Redis } from 'ioredis';

interface DashboardClient {
  ws: WebSocket;
  userId: string;
  subscriptions: Set<string>; // Topics die der Client abonniert hat
}

class DashboardServer {
  private wss: WebSocketServer;
  private clients = new Map<string, DashboardClient>();
  private redis: Redis;
  private redisSub: Redis;

  constructor(server: any) {
    this.wss = new WebSocketServer({ server, path: '/dashboard' });
    this.redis = new Redis(process.env.REDIS_URL!);
    this.redisSub = new Redis(process.env.REDIS_URL!);

    this.setupWebSocket();
    this.setupRedisSubscription();
  }

  private setupWebSocket() {
    this.wss.on('connection', (ws, req) => {
      const clientId = crypto.randomUUID();
      const userId = this.extractUserId(req);

      const client: DashboardClient = {
        ws,
        userId,
        subscriptions: new Set()
      };

      this.clients.set(clientId, client);
      console.log(`Dashboard client connected: ${clientId}`);

      ws.on('message', (data) => {
        this.handleClientMessage(clientId, JSON.parse(data.toString()));
      });

      ws.on('close', () => {
        this.clients.delete(clientId);
        console.log(`Dashboard client disconnected: ${clientId}`);
      });

      // Initial Data senden
      this.sendInitialData(client);
    });
  }

  private handleClientMessage(clientId: string, message: any) {
    const client = this.clients.get(clientId);
    if (!client) return;

    switch (message.type) {
      case 'subscribe':
        this.subscribeToTopic(client, message.topic);
        break;

      case 'unsubscribe':
        this.unsubscribeFromTopic(client, message.topic);
        break;

      case 'request-data':
        this.sendDataForTopic(client, message.topic);
        break;
    }
  }

  private subscribeToTopic(client: DashboardClient, topic: string) {
    client.subscriptions.add(topic);
    console.log(`Client subscribed to: ${topic}`);
  }

  private unsubscribeFromTopic(client: DashboardClient, topic: string) {
    client.subscriptions.delete(topic);
  }

  private setupRedisSubscription() {
    // Auf Data-Updates hören
    this.redisSub.psubscribe('dashboard:*');

    this.redisSub.on('pmessage', (pattern, channel, message) => {
      const topic = channel.replace('dashboard:', '');
      const data = JSON.parse(message);

      this.broadcastToSubscribers(topic, data);
    });
  }

  private broadcastToSubscribers(topic: string, data: any) {
    this.clients.forEach((client) => {
      if (client.subscriptions.has(topic) &&
          client.ws.readyState === WebSocket.OPEN) {
        client.ws.send(JSON.stringify({
          type: 'data-update',
          topic,
          data,
          timestamp: Date.now()
        }));
      }
    });
  }

  // Externe Data-Updates publishen
  async publishUpdate(topic: string, data: any) {
    await this.redis.publish(`dashboard:${topic}`, JSON.stringify(data));
  }

  private async sendInitialData(client: DashboardClient) {
    // Standard-Metriken laden
    const metrics = await this.loadInitialMetrics();

    client.ws.send(JSON.stringify({
      type: 'initial-data',
      data: metrics
    }));
  }

  private async loadInitialMetrics() {
    // Letzte Werte aus Redis laden
    const keys = ['sales', 'users', 'orders', 'revenue'];
    const values = await Promise.all(
      keys.map(k => this.redis.get(`metrics:${k}`))
    );

    return keys.reduce((acc, key, i) => {
      acc[key] = JSON.parse(values[i] || '{}');
      return acc;
    }, {} as Record<string, any>);
  }
}

Data Publisher Service

// src/services/metrics-publisher.ts
import { Redis } from 'ioredis';

class MetricsPublisher {
  private redis: Redis;

  constructor() {
    this.redis = new Redis(process.env.REDIS_URL!);
  }

  async publishSalesUpdate(data: SalesData) {
    // In Redis speichern (für neue Clients)
    await this.redis.set('metrics:sales', JSON.stringify(data));

    // An alle Subscriber publishen
    await this.redis.publish('dashboard:sales', JSON.stringify(data));
  }

  async publishUserStats(data: UserStats) {
    await this.redis.set('metrics:users', JSON.stringify(data));
    await this.redis.publish('dashboard:users', JSON.stringify(data));
  }

  async publishOrderMetrics(data: OrderMetrics) {
    await this.redis.set('metrics:orders', JSON.stringify(data));
    await this.redis.publish('dashboard:orders', JSON.stringify(data));
  }

  // Batch-Updates für mehrere Metriken
  async publishBatch(updates: Record<string, any>) {
    const pipeline = this.redis.pipeline();

    for (const [topic, data] of Object.entries(updates)) {
      pipeline.set(`metrics:${topic}`, JSON.stringify(data));
      pipeline.publish(`dashboard:${topic}`, JSON.stringify(data));
    }

    await pipeline.exec();
  }
}

// Beispiel: Periodische Updates
const publisher = new MetricsPublisher();

setInterval(async () => {
  const liveMetrics = await fetchLiveMetrics();
  await publisher.publishBatch(liveMetrics);
}, 1000); // Jede Sekunde

React Dashboard Client

// src/hooks/useDashboard.ts
import { useEffect, useState, useCallback, useRef } from 'react';

interface DashboardData {
  sales: SalesData | null;
  users: UserStats | null;
  orders: OrderMetrics | null;
  revenue: RevenueData | null;
}

export function useDashboard() {
  const wsRef = useRef<WebSocket | null>(null);
  const [isConnected, setIsConnected] = useState(false);
  const [data, setData] = useState<DashboardData>({
    sales: null,
    users: null,
    orders: null,
    revenue: null
  });

  useEffect(() => {
    const ws = new WebSocket(`${process.env.NEXT_PUBLIC_WS_URL}/dashboard`);
    wsRef.current = ws;

    ws.onopen = () => {
      setIsConnected(true);
      // Alle Topics abonnieren
      ws.send(JSON.stringify({ type: 'subscribe', topic: 'sales' }));
      ws.send(JSON.stringify({ type: 'subscribe', topic: 'users' }));
      ws.send(JSON.stringify({ type: 'subscribe', topic: 'orders' }));
      ws.send(JSON.stringify({ type: 'subscribe', topic: 'revenue' }));
    };

    ws.onmessage = (event) => {
      const message = JSON.parse(event.data);

      switch (message.type) {
        case 'initial-data':
          setData(message.data);
          break;

        case 'data-update':
          setData(prev => ({
            ...prev,
            [message.topic]: message.data
          }));
          break;
      }
    };

    ws.onclose = () => {
      setIsConnected(false);
      // Reconnection Logic
      setTimeout(() => {
        // Reconnect...
      }, 3000);
    };

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

  const subscribe = useCallback((topic: string) => {
    wsRef.current?.send(JSON.stringify({ type: 'subscribe', topic }));
  }, []);

  const unsubscribe = useCallback((topic: string) => {
    wsRef.current?.send(JSON.stringify({ type: 'unsubscribe', topic }));
  }, []);

  return {
    data,
    isConnected,
    subscribe,
    unsubscribe
  };
}

Dashboard Component

// src/components/Dashboard.tsx
import { useDashboard } from '../hooks/useDashboard';
import { LineChart, BarChart, StatCard } from './charts';

export function Dashboard() {
  const { data, isConnected } = useDashboard();

  return (
    <div className="p-6 bg-gray-100 min-h-screen">
      {/* Connection Status */}
      <div className={`mb-4 px-4 py-2 rounded ${
        isConnected ? 'bg-green-500' : 'bg-red-500'
      } text-white`}>
        {isConnected ? '🟢 Live' : '🔴 Reconnecting...'}
      </div>

      {/* Stats Grid */}
      <div className="grid grid-cols-4 gap-4 mb-6">
        <StatCard
          title="Umsatz heute"
          value={data.revenue?.today || 0}
          change={data.revenue?.changePercent || 0}
          format="currency"
        />
        <StatCard
          title="Bestellungen"
          value={data.orders?.count || 0}
          change={data.orders?.changePercent || 0}
        />
        <StatCard
          title="Aktive User"
          value={data.users?.active || 0}
          change={data.users?.changePercent || 0}
        />
        <StatCard
          title="Conversion Rate"
          value={data.sales?.conversionRate || 0}
          change={data.sales?.changePercent || 0}
          format="percent"
        />
      </div>

      {/* Charts */}
      <div className="grid grid-cols-2 gap-6">
        <div className="bg-white rounded-lg p-4 shadow">
          <h3 className="font-semibold mb-4">Umsatz (Live)</h3>
          <LineChart
            data={data.revenue?.timeline || []}
            animated={true}
          />
        </div>

        <div className="bg-white rounded-lg p-4 shadow">
          <h3 className="font-semibold mb-4">Bestellungen pro Stunde</h3>
          <BarChart
            data={data.orders?.hourly || []}
            animated={true}
          />
        </div>
      </div>
    </div>
  );
}

Animated Chart Component

// src/components/charts/AnimatedLineChart.tsx
import { useEffect, useRef, useState } from 'react';

interface DataPoint {
  timestamp: number;
  value: number;
}

export function AnimatedLineChart({
  data,
  maxPoints = 60
}: {
  data: DataPoint[];
  maxPoints?: number;
}) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const [displayData, setDisplayData] = useState<DataPoint[]>([]);

  // Neue Datenpunkte animiert hinzufügen
  useEffect(() => {
    if (data.length > displayData.length) {
      // Neuen Punkt hinzufügen
      setDisplayData(prev => {
        const newData = [...prev, data[data.length - 1]];
        // Max Points einhalten
        if (newData.length > maxPoints) {
          return newData.slice(-maxPoints);
        }
        return newData;
      });
    }
  }, [data]);

  // Canvas rendern
  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas || displayData.length === 0) return;

    const ctx = canvas.getContext('2d')!;
    const width = canvas.width;
    const height = canvas.height;

    // Clear
    ctx.clearRect(0, 0, width, height);

    // Skalierung berechnen
    const values = displayData.map(d => d.value);
    const min = Math.min(...values);
    const max = Math.max(...values);
    const range = max - min || 1;

    // Line zeichnen
    ctx.beginPath();
    ctx.strokeStyle = '#3b82f6';
    ctx.lineWidth = 2;

    displayData.forEach((point, i) => {
      const x = (i / (maxPoints - 1)) * width;
      const y = height - ((point.value - min) / range) * height;

      if (i === 0) {
        ctx.moveTo(x, y);
      } else {
        ctx.lineTo(x, y);
      }
    });

    ctx.stroke();

    // Gradient Fill
    ctx.lineTo(width, height);
    ctx.lineTo(0, height);
    ctx.closePath();

    const gradient = ctx.createLinearGradient(0, 0, 0, height);
    gradient.addColorStop(0, 'rgba(59, 130, 246, 0.3)');
    gradient.addColorStop(1, 'rgba(59, 130, 246, 0)');
    ctx.fillStyle = gradient;
    ctx.fill();

  }, [displayData, maxPoints]);

  return (
    <canvas
      ref={canvasRef}
      width={600}
      height={200}
      className="w-full h-48"
    />
  );
}

Performance-Optimierungen

1. Throttling für High-Frequency Updates

function throttle<T>(
  fn: (data: T) => void,
  intervalMs: number
): (data: T) => void {
  let lastData: T | null = null;
  let scheduled = false;

  return (data: T) => {
    lastData = data;

    if (!scheduled) {
      scheduled = true;
      setTimeout(() => {
        fn(lastData!);
        scheduled = false;
      }, intervalMs);
    }
  };
}

// Verwendung: Max 10 Updates pro Sekunde
const throttledUpdate = throttle((data) => {
  setData(data);
}, 100);

2. Delta Updates

// Nur Änderungen senden
function createDelta(previous: any, current: any): any {
  const delta: any = {};

  for (const key of Object.keys(current)) {
    if (JSON.stringify(previous[key]) !== JSON.stringify(current[key])) {
      delta[key] = current[key];
    }
  }

  return Object.keys(delta).length > 0 ? delta : null;
}

3. Web Worker für Datenverarbeitung

// worker.ts
self.onmessage = (event) => {
  const { type, data } = event.data;

  if (type === 'process-metrics') {
    // Schwere Berechnung im Worker
    const processed = processHeavyData(data);
    self.postMessage({ type: 'metrics-processed', data: processed });
  }
};

Fazit

Real-Time Dashboards mit WebSockets bieten:

  1. Instant Updates: Daten erscheinen sofort
  2. Weniger Load: Keine unnötigen Requests
  3. Bessere UX: Flüssige Animationen möglich
  4. Skalierbarkeit: Redis Pub/Sub für Multi-Server

Für datenintensive Dashboards ist WebSocket-Push der Standard 2026.


Bildprompts

  1. "Dashboard with live updating charts and metrics, data flowing in real-time, modern analytics interface"
  2. "WebSocket connection stream feeding into dashboard, visualization of data flow"
  3. "Multiple dashboard widgets updating simultaneously, business analytics, clean dark theme"

Quellen