1 min read
PerformanceReal-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
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 SekundeReact 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:
- Instant Updates: Daten erscheinen sofort
- Weniger Load: Keine unnötigen Requests
- Bessere UX: Flüssige Animationen möglich
- Skalierbarkeit: Redis Pub/Sub für Multi-Server
Für datenintensive Dashboards ist WebSocket-Push der Standard 2026.
Bildprompts
- "Dashboard with live updating charts and metrics, data flowing in real-time, modern analytics interface"
- "WebSocket connection stream feeding into dashboard, visualization of data flow"
- "Multiple dashboard widgets updating simultaneously, business analytics, clean dark theme"