# 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 ```typescript // src/dashboard-server.ts import { WebSocketServer, WebSocket } from 'ws'; import { Redis } from 'ioredis'; interface DashboardClient { ws: WebSocket; userId: string; subscriptions: Set; // Topics die der Client abonniert hat } class DashboardServer { private wss: WebSocketServer; private clients = new Map(); 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); } } ``` --- ## Data Publisher Service ```typescript // 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) { 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 ```typescript // 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(null); const [isConnected, setIsConnected] = useState(false); const [data, setData] = useState({ 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 ```tsx // src/components/Dashboard.tsx import { useDashboard } from '../hooks/useDashboard'; import { LineChart, BarChart, StatCard } from './charts'; export function Dashboard() { const { data, isConnected } = useDashboard(); return (
{/* Connection Status */}
{isConnected ? '🟢 Live' : '🔴 Reconnecting...'}
{/* Stats Grid */}
{/* Charts */}

Umsatz (Live)

Bestellungen pro Stunde

); } ``` --- ## Animated Chart Component ```tsx // 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(null); const [displayData, setDisplayData] = useState([]); // 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 ( ); } ``` --- ## Performance-Optimierungen ### 1. Throttling für High-Frequency Updates ```typescript function throttle( 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 ```typescript // 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 ```typescript // 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 - [WebSocket API (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) - [Redis Pub/Sub](https://redis.io/docs/manual/pubsub/) - [React Real-Time Charts](https://recharts.org/) - [Canvas API Performance](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Optimizing_canvas)