Real-time data is React’s kryptonite. Stock prices updating 100 times per second, chat messages flooding in, live collaboration with dozens of users—each update triggers a re-render, and suddenly your smooth React app becomes a stuttering mess. The challenge isn’t the network; modern WebSockets can handle thousands of messages per second. The challenge is React’s reconciliation process choking on the frequency of updates.
But real-time doesn’t have to mean real-slow. With the right patterns—throttling, batching, virtualization, and selective updates—you can handle massive data streams while maintaining 60fps. The key is understanding where the bottlenecks are (hint: it’s not always where you think) and implementing the right optimizations at each layer of your stack. This guide shows you how to build React applications that handle real-time data like a pro.
Understanding Real-Time Performance Challenges
Real-time data creates unique performance problems:
// Real-time data performance characteristics
interface RealTimePerformance {
// Performance challenges
challenges: {
updateFrequency: 'Updates faster than frame rate (>60Hz)';
reconciliation: 'React diffing overhead on every update';
memoryGrowth: 'Unbounded data accumulation';
stateManagement: 'State updates triggering cascading renders';
networkBackpressure: 'Client cant process messages fast enough';
};
// Key metrics
metrics: {
messagesPerSecond: number; // Incoming message rate
updatesPerSecond: number; // React update rate
droppedFrames: number; // Frames below 60fps
memoryUsage: number; // Heap size over time
latency: number; // Message to render time
};
// Optimization strategies
strategies: {
throttling: 'Limit update frequency';
batching: 'Group multiple updates';
virtualization: 'Render only visible items';
webWorkers: 'Process data off main thread';
selective: 'Update only changed components';
};
}
// Performance impact by update frequency
const updateFrequencyImpact = {
'1Hz': { impact: 'Negligible', strategy: 'Direct updates' },
'10Hz': { impact: 'Low', strategy: 'Basic throttling' },
'60Hz': { impact: 'Medium', strategy: 'Batching + throttling' },
'100Hz': { impact: 'High', strategy: 'Web Workers + virtualization' },
'1000Hz': { impact: 'Extreme', strategy: 'Sampling + aggregation' },
};WebSocket Connection Management
Efficient WebSocket setup with reconnection and backpressure:
// Advanced WebSocket manager
class WebSocketManager {
private ws: WebSocket | null = null;
private url: string;
private messageQueue: any[] = [];
private subscribers: Map<string, Set<MessageHandler>> = new Map();
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
private reconnectDelay = 1000;
private isConnected = false;
private stats = {
messagesReceived: 0,
messagesSent: 0,
bytesReceived: 0,
bytesSent: 0,
connectTime: 0,
disconnects: 0,
};
constructor(url: string) {
this.url = url;
this.connect();
}
private connect() {
try {
this.ws = new WebSocket(this.url);
this.setupEventHandlers();
} catch (error) {
console.error('WebSocket connection failed:', error);
this.scheduleReconnect();
}
}
private setupEventHandlers() {
if (!this.ws) return;
this.ws.onopen = () => {
console.log('WebSocket connected');
this.isConnected = true;
this.reconnectAttempts = 0;
this.stats.connectTime = Date.now();
// Flush queued messages
while (this.messageQueue.length > 0) {
const message = this.messageQueue.shift();
this.send(message);
}
this.emit('connection', { status: 'connected' });
};
this.ws.onmessage = (event) => {
this.stats.messagesReceived++;
this.stats.bytesReceived += event.data.length;
try {
const data = JSON.parse(event.data);
this.handleMessage(data);
} catch (error) {
console.error('Failed to parse WebSocket message:', error);
}
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
this.emit('error', error);
};
this.ws.onclose = () => {
console.log('WebSocket disconnected');
this.isConnected = false;
this.stats.disconnects++;
this.emit('connection', { status: 'disconnected' });
this.scheduleReconnect();
};
}
private handleMessage(data: any) {
const { type, payload } = data;
// Handle system messages
if (type === 'ping') {
this.send({ type: 'pong', timestamp: Date.now() });
return;
}
// Emit to subscribers
this.emit(type, payload);
}
private emit(event: string, data: any) {
const handlers = this.subscribers.get(event);
if (handlers) {
handlers.forEach((handler) => {
try {
handler(data);
} catch (error) {
console.error(`Error in WebSocket handler for ${event}:`, error);
}
});
}
// Also emit wildcard
const wildcardHandlers = this.subscribers.get('*');
if (wildcardHandlers) {
wildcardHandlers.forEach((handler) => handler({ type: event, data }));
}
}
subscribe(event: string, handler: MessageHandler): () => void {
if (!this.subscribers.has(event)) {
this.subscribers.set(event, new Set());
}
this.subscribers.get(event)!.add(handler);
// Return unsubscribe function
return () => {
const handlers = this.subscribers.get(event);
if (handlers) {
handlers.delete(handler);
if (handlers.size === 0) {
this.subscribers.delete(event);
}
}
};
}
send(data: any) {
const message = JSON.stringify(data);
if (this.isConnected && this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(message);
this.stats.messagesSent++;
this.stats.bytesSent += message.length;
} else {
// Queue message for later
this.messageQueue.push(data);
// Limit queue size to prevent memory issues
if (this.messageQueue.length > 1000) {
this.messageQueue.shift(); // Drop oldest message
console.warn('WebSocket message queue overflow');
}
}
}
private scheduleReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('Max reconnection attempts reached');
this.emit('error', new Error('Connection failed'));
return;
}
const delay = Math.min(this.reconnectDelay * Math.pow(2, this.reconnectAttempts), 30000);
this.reconnectAttempts++;
setTimeout(() => {
console.log(`Reconnecting... (attempt ${this.reconnectAttempts})`);
this.connect();
}, delay);
}
getStats() {
return {
...this.stats,
queueSize: this.messageQueue.length,
isConnected: this.isConnected,
uptime: this.isConnected ? Date.now() - this.stats.connectTime : 0,
};
}
close() {
this.ws?.close();
this.ws = null;
}
}
// React hook for WebSocket
export function useWebSocket(url: string) {
const [manager] = useState(() => new WebSocketManager(url));
const [isConnected, setIsConnected] = useState(false);
const [stats, setStats] = useState(manager.getStats());
useEffect(() => {
const unsubscribe = manager.subscribe('connection', ({ status }) => {
setIsConnected(status === 'connected');
});
// Update stats periodically
const interval = setInterval(() => {
setStats(manager.getStats());
}, 1000);
return () => {
unsubscribe();
clearInterval(interval);
manager.close();
};
}, [manager]);
return {
subscribe: manager.subscribe.bind(manager),
send: manager.send.bind(manager),
isConnected,
stats,
};
}Throttling and Batching Updates
Control update frequency to maintain performance:
// Advanced throttling and batching
class UpdateBatcher<T> {
private batch: T[] = [];
private timer: NodeJS.Timeout | null = null;
private lastFlush = 0;
constructor(
private callback: (batch: T[]) => void,
private options: {
maxBatchSize: number;
maxWaitTime: number;
throttleMs: number;
},
) {}
add(item: T) {
this.batch.push(item);
// Flush if batch is full
if (this.batch.length >= this.options.maxBatchSize) {
this.flush();
return;
}
// Schedule flush if not already scheduled
if (!this.timer) {
this.timer = setTimeout(() => {
this.flush();
}, this.options.maxWaitTime);
}
}
private flush() {
if (this.batch.length === 0) return;
// Enforce throttling
const now = Date.now();
const timeSinceLastFlush = now - this.lastFlush;
if (timeSinceLastFlush < this.options.throttleMs) {
// Reschedule flush
if (this.timer) clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.flush();
}, this.options.throttleMs - timeSinceLastFlush);
return;
}
// Process batch
const batchToProcess = this.batch;
this.batch = [];
this.lastFlush = now;
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
this.callback(batchToProcess);
}
destroy() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
this.batch = [];
}
}
// React hook for batched updates
export function useBatchedUpdates<T>(
processor: (batch: T[]) => void,
options = {
maxBatchSize: 100,
maxWaitTime: 100,
throttleMs: 16, // One frame
},
) {
const batcherRef = useRef<UpdateBatcher<T> | null>(null);
useEffect(() => {
batcherRef.current = new UpdateBatcher(processor, options);
return () => {
batcherRef.current?.destroy();
};
}, [processor, options]);
const add = useCallback((item: T) => {
batcherRef.current?.add(item);
}, []);
return { add };
}
// Throttled state updates
export function useThrottledState<T>(
initialValue: T,
throttleMs = 100,
): [T, (value: T) => void, T] {
const [value, setValue] = useState(initialValue);
const [displayValue, setDisplayValue] = useState(initialValue);
const lastUpdateRef = useRef(0);
const pendingValueRef = useRef<T | null>(null);
const timerRef = useRef<NodeJS.Timeout | null>(null);
const throttledSetValue = useCallback(
(newValue: T) => {
setValue(newValue);
pendingValueRef.current = newValue;
const now = Date.now();
const timeSinceLastUpdate = now - lastUpdateRef.current;
if (timeSinceLastUpdate >= throttleMs) {
// Update immediately
setDisplayValue(newValue);
lastUpdateRef.current = now;
pendingValueRef.current = null;
} else {
// Schedule update
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
if (pendingValueRef.current !== null) {
setDisplayValue(pendingValueRef.current);
lastUpdateRef.current = Date.now();
pendingValueRef.current = null;
}
}, throttleMs - timeSinceLastUpdate);
}
},
[throttleMs],
);
useEffect(() => {
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
}, []);
return [displayValue, throttledSetValue, value];
}Virtualization for Large Data Sets
Handle large real-time lists efficiently:
// Virtual list for real-time data
function RealTimeVirtualList<T>({
items,
itemHeight,
height,
renderItem,
overscan = 3,
}: {
items: T[];
itemHeight: number;
height: number;
renderItem: (item: T, index: number) => React.ReactNode;
overscan?: number;
}) {
const [scrollTop, setScrollTop] = useState(0);
const scrollRef = useRef<HTMLDivElement>(null);
// Calculate visible range
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
const endIndex = Math.min(
items.length - 1,
Math.ceil((scrollTop + height) / itemHeight) + overscan,
);
const visibleItems = items.slice(startIndex, endIndex + 1);
const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
setScrollTop(e.currentTarget.scrollTop);
}, []);
// Auto-scroll to bottom for new messages
const [autoScroll, setAutoScroll] = useState(true);
const prevItemsLength = useRef(items.length);
useEffect(() => {
if (autoScroll && items.length > prevItemsLength.current) {
scrollRef.current?.scrollTo({
top: items.length * itemHeight,
behavior: 'smooth',
});
}
prevItemsLength.current = items.length;
}, [items.length, autoScroll, itemHeight]);
const handleUserScroll = useCallback(() => {
if (!scrollRef.current) return;
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
const isAtBottom = scrollHeight - scrollTop - clientHeight < 10;
setAutoScroll(isAtBottom);
}, []);
return (
<div
ref={scrollRef}
style={{ height, overflow: 'auto' }}
onScroll={(e) => {
handleScroll(e);
handleUserScroll();
}}
>
<div style={{ height: items.length * itemHeight, position: 'relative' }}>
{visibleItems.map((item, i) => (
<div
key={startIndex + i}
style={{
position: 'absolute',
top: (startIndex + i) * itemHeight,
left: 0,
right: 0,
height: itemHeight,
}}
>
{renderItem(item, startIndex + i)}
</div>
))}
</div>
</div>
);
}Selective Component Updates
Update only what’s changed:
// Selective update system
class SelectiveUpdateManager {
private subscriptions: Map<string, Set<() => void>> = new Map();
private data: Map<string, any> = new Map();
update(path: string, value: any) {
const oldValue = this.data.get(path);
if (oldValue === value) return; // No change
this.data.set(path, value);
// Notify only subscribers to this path
const subscribers = this.subscriptions.get(path);
if (subscribers) {
subscribers.forEach((callback) => callback());
}
// Notify wildcard subscribers
const wildcardPath = path.split('.').slice(0, -1).join('.') + '.*';
const wildcardSubscribers = this.subscriptions.get(wildcardPath);
if (wildcardSubscribers) {
wildcardSubscribers.forEach((callback) => callback());
}
}
subscribe(path: string, callback: () => void): () => void {
if (!this.subscriptions.has(path)) {
this.subscriptions.set(path, new Set());
}
this.subscriptions.get(path)!.add(callback);
return () => {
const subs = this.subscriptions.get(path);
if (subs) {
subs.delete(callback);
if (subs.size === 0) {
this.subscriptions.delete(path);
}
}
};
}
get(path: string): any {
return this.data.get(path);
}
}
// React hook for selective updates
export function useSelectiveUpdate<T>(path: string, initialValue: T): T {
const manager = useRef(new SelectiveUpdateManager());
const [value, setValue] = useState<T>(() => manager.current.get(path) ?? initialValue);
useEffect(() => {
return manager.current.subscribe(path, () => {
setValue(manager.current.get(path));
});
}, [path]);
return value;
}
// Memoized real-time component
const RealTimeDataRow = memo(
function RealTimeDataRow({ data, fields }: { data: any; fields: string[] }) {
return (
<div className="data-row">
{fields.map((field) => (
<RealTimeCell key={field} path={`${data.id}.${field}`} />
))}
</div>
);
},
(prevProps, nextProps) => {
// Custom comparison - only re-render if specified fields change
return prevProps.fields.every((field) => prevProps.data[field] === nextProps.data[field]);
},
);
function RealTimeCell({ path }: { path: string }) {
const value = useSelectiveUpdate(path, null);
return <div className="data-cell">{value}</div>;
}Server-Sent Events (SSE)
Alternative to WebSockets for one-way streaming:
// SSE manager for real-time updates
class SSEManager {
private eventSource: EventSource | null = null;
private subscribers: Map<string, Set<(data: any) => void>> = new Map();
private reconnectAttempts = 0;
private lastEventId: string | null = null;
constructor(private url: string) {
this.connect();
}
private connect() {
const url = new URL(this.url);
if (this.lastEventId) {
url.searchParams.set('lastEventId', this.lastEventId);
}
this.eventSource = new EventSource(url.toString());
this.eventSource.onopen = () => {
console.log('SSE connected');
this.reconnectAttempts = 0;
this.emit('connected', null);
};
this.eventSource.onerror = (error) => {
console.error('SSE error:', error);
if (this.eventSource?.readyState === EventSource.CLOSED) {
this.reconnect();
}
};
this.eventSource.onmessage = (event) => {
this.lastEventId = event.lastEventId;
try {
const data = JSON.parse(event.data);
this.emit('message', data);
} catch (error) {
console.error('Failed to parse SSE message:', error);
}
};
// Custom event handlers
this.eventSource.addEventListener('update', (event: any) => {
const data = JSON.parse(event.data);
this.emit('update', data);
});
this.eventSource.addEventListener('delete', (event: any) => {
const data = JSON.parse(event.data);
this.emit('delete', data);
});
}
private emit(event: string, data: any) {
const handlers = this.subscribers.get(event);
if (handlers) {
handlers.forEach((handler) => {
try {
handler(data);
} catch (error) {
console.error(`Error in SSE handler for ${event}:`, error);
}
});
}
}
subscribe(event: string, handler: (data: any) => void): () => void {
if (!this.subscribers.has(event)) {
this.subscribers.set(event, new Set());
}
this.subscribers.get(event)!.add(handler);
return () => {
const handlers = this.subscribers.get(event);
if (handlers) {
handlers.delete(handler);
if (handlers.size === 0) {
this.subscribers.delete(event);
}
}
};
}
private reconnect() {
if (this.reconnectAttempts >= 5) {
console.error('Max SSE reconnection attempts reached');
return;
}
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
this.reconnectAttempts++;
setTimeout(() => {
console.log(`SSE reconnecting... (attempt ${this.reconnectAttempts})`);
this.connect();
}, delay);
}
close() {
this.eventSource?.close();
this.eventSource = null;
}
}
// React hook for SSE
export function useSSE(url: string) {
const [manager] = useState(() => new SSEManager(url));
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
const unsubConnect = manager.subscribe('connected', () => {
setIsConnected(true);
});
const unsubError = manager.subscribe('error', () => {
setIsConnected(false);
});
return () => {
unsubConnect();
unsubError();
manager.close();
};
}, [manager]);
return {
subscribe: manager.subscribe.bind(manager),
isConnected,
};
}Web Worker Processing
Process real-time data off the main thread:
// worker/realtime-processor.ts
const ctx: Worker = self as any;
interface DataPoint {
id: string;
timestamp: number;
value: number;
}
class RealTimeProcessor {
private buffer: DataPoint[] = [];
private aggregates: Map<string, any> = new Map();
process(data: DataPoint) {
this.buffer.push(data);
// Keep buffer size limited
if (this.buffer.length > 10000) {
this.buffer.shift();
}
// Update aggregates
this.updateAggregates(data);
// Send processed data back
ctx.postMessage({
type: 'processed',
data: {
latest: data,
aggregates: Object.fromEntries(this.aggregates),
bufferSize: this.buffer.length,
},
});
}
private updateAggregates(data: DataPoint) {
const window = 60000; // 1 minute window
const now = Date.now();
const windowData = this.buffer.filter((d) => now - d.timestamp < window);
const values = windowData.map((d) => d.value);
this.aggregates.set('count', windowData.length);
this.aggregates.set(
'sum',
values.reduce((a, b) => a + b, 0),
);
this.aggregates.set('avg', this.aggregates.get('sum') / windowData.length);
this.aggregates.set('min', Math.min(...values));
this.aggregates.set('max', Math.max(...values));
}
batch(dataPoints: DataPoint[]) {
dataPoints.forEach((point) => this.process(point));
}
}
const processor = new RealTimeProcessor();
ctx.addEventListener('message', (event) => {
const { type, data } = event.data;
switch (type) {
case 'process':
processor.process(data);
break;
case 'batch':
processor.batch(data);
break;
}
});
// React hook for worker processing
export function useRealTimeWorker() {
const workerRef = useRef<Worker | null>(null);
const [aggregates, setAggregates] = useState<any>({});
useEffect(() => {
workerRef.current = new Worker(new URL('../workers/realtime-processor.ts', import.meta.url), {
type: 'module',
});
workerRef.current.onmessage = (event) => {
if (event.data.type === 'processed') {
setAggregates(event.data.data.aggregates);
}
};
return () => {
workerRef.current?.terminate();
};
}, []);
const process = useCallback((data: DataPoint) => {
workerRef.current?.postMessage({ type: 'process', data });
}, []);
const processBatch = useCallback((data: DataPoint[]) => {
workerRef.current?.postMessage({ type: 'batch', data });
}, []);
return { process, processBatch, aggregates };
}Performance Monitoring
Track real-time performance metrics:
// Real-time performance monitor
class RealTimePerformanceMonitor {
private metrics = {
messagesPerSecond: 0,
updatesPerSecond: 0,
droppedFrames: 0,
avgLatency: 0,
maxLatency: 0,
memoryUsage: 0,
};
private messageTimestamps: number[] = [];
private updateTimestamps: number[] = [];
private frameDrops = 0;
private lastFrameTime = performance.now();
trackMessage() {
const now = Date.now();
this.messageTimestamps.push(now);
// Keep only last second
this.messageTimestamps = this.messageTimestamps.filter((t) => now - t < 1000);
this.metrics.messagesPerSecond = this.messageTimestamps.length;
}
trackUpdate(latency: number) {
const now = Date.now();
this.updateTimestamps.push(now);
// Keep only last second
this.updateTimestamps = this.updateTimestamps.filter((t) => now - t < 1000);
this.metrics.updatesPerSecond = this.updateTimestamps.length;
// Update latency metrics
this.metrics.avgLatency = this.metrics.avgLatency * 0.9 + latency * 0.1;
this.metrics.maxLatency = Math.max(this.metrics.maxLatency, latency);
}
trackFrame() {
const now = performance.now();
const frameDuration = now - this.lastFrameTime;
if (frameDuration > 16.67) {
// Slower than 60fps
this.frameDrops++;
this.metrics.droppedFrames = this.frameDrops;
}
this.lastFrameTime = now;
}
trackMemory() {
if (performance.memory) {
this.metrics.memoryUsage = performance.memory.usedJSHeapSize;
}
}
getMetrics() {
this.trackMemory();
return { ...this.metrics };
}
reset() {
this.messageTimestamps = [];
this.updateTimestamps = [];
this.frameDrops = 0;
this.metrics.maxLatency = 0;
}
}
// React component for monitoring
function RealTimeMonitor() {
const monitor = useRef(new RealTimePerformanceMonitor());
const [metrics, setMetrics] = useState(monitor.current.getMetrics());
useEffect(() => {
const interval = setInterval(() => {
setMetrics(monitor.current.getMetrics());
}, 1000);
const frameInterval = setInterval(() => {
monitor.current.trackFrame();
}, 16);
return () => {
clearInterval(interval);
clearInterval(frameInterval);
};
}, []);
return (
<div className="realtime-monitor">
<div>Messages/sec: {metrics.messagesPerSecond}</div>
<div>Updates/sec: {metrics.updatesPerSecond}</div>
<div>Dropped frames: {metrics.droppedFrames}</div>
<div>Avg latency: {metrics.avgLatency.toFixed(2)}ms</div>
<div>Max latency: {metrics.maxLatency.toFixed(2)}ms</div>
<div>Memory: {(metrics.memoryUsage / 1024 / 1024).toFixed(2)}MB</div>
</div>
);
}Best Practices Checklist
interface RealTimeBestPractices {
// Connection management
connection: {
autoReconnect: 'Implement automatic reconnection with backoff';
heartbeat: 'Send periodic heartbeats to detect disconnections';
queueMessages: 'Queue messages during disconnection';
compression: 'Enable compression for large payloads';
};
// Update optimization
updates: {
throttleUpdates: 'Limit update frequency to 60fps max';
batchUpdates: 'Group multiple updates together';
selectiveUpdates: 'Update only changed components';
virtualizeList: 'Use virtualization for large lists';
};
// Memory management
memory: {
limitBuffer: 'Cap message buffer size';
clearOldData: 'Remove data outside viewport';
unsubscribe: 'Clean up subscriptions on unmount';
monitorGrowth: 'Track memory usage over time';
};
// Performance
performance: {
useWorkers: 'Process data in Web Workers';
debounceRenders: 'Debounce rapid state changes';
memoComponents: 'Memoize expensive components';
profileRegularly: 'Monitor performance metrics';
};
}