Performance Optimization
Optimizing real-time application performance is crucial for scalability and user experience. In this lesson, we'll explore techniques to maximize efficiency in WebSocket and Socket.io applications.
Binary Data Transfer
Binary data transfer is significantly more efficient than JSON for certain types of data, reducing bandwidth and improving speed.
// Sending binary data with WebSocket
const ws = new WebSocket('ws://localhost:8080');
// Send ArrayBuffer
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
view.setInt32(0, 12345);
view.setFloat32(4, 67.89);
ws.send(buffer);
// Send Blob (for files/images)
fetch('image.jpg')
.then(response => response.blob())
.then(blob => {
ws.send(blob);
});
// Receive binary data
ws.binaryType = 'arraybuffer'; // or 'blob'
ws.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
const view = new DataView(event.data);
const number = view.getInt32(0);
const float = view.getFloat32(4);
console.log('Received:', number, float);
}
};
// Socket.io with binary data
const socket = io();
// Send binary
const uint8Array = new Uint8Array([1, 2, 3, 4, 5]);
socket.emit('binary-data', uint8Array);
// Receive binary
socket.on('image-data', (buffer) => {
const blob = new Blob([buffer], { type: 'image/jpeg' });
const url = URL.createObjectURL(blob);
document.getElementById('img').src = url;
});
Performance Tip: Binary data is 30-50% smaller than equivalent JSON for numerical data. Use for game state, sensor data, images, audio, and video streams.
Message Compression (perMessageDeflate)
Message compression reduces bandwidth usage by compressing WebSocket frames automatically.
// WebSocket server with compression
const WebSocket = require('ws');
const wss = new WebSocket.Server({
port: 8080,
perMessageDeflate: {
zlibDeflateOptions: {
chunkSize: 1024,
memLevel: 7,
level: 3 // Compression level (0-9)
},
zlibInflateOptions: {
chunkSize: 10 * 1024
},
clientNoContextTakeover: true,
serverNoContextTakeover: true,
serverMaxWindowBits: 10,
concurrencyLimit: 10,
threshold: 1024 // Only compress messages > 1KB
}
});
// Socket.io with compression
const io = require('socket.io')(3000, {
perMessageDeflate: {
threshold: 1024,
zlibDeflateOptions: {
chunkSize: 8 * 1024,
level: 6
}
}
});
// Disable compression for specific messages
socket.compress(false).emit('small-message', 'hello');
socket.compress(true).emit('large-message', largeDataObject);
// Benchmark: Compression effectiveness
const originalData = {
users: Array(1000).fill(null).map((_, i) => ({
id: i,
name: `User ${i}`,
email: `user${i}@example.com`,
timestamp: Date.now()
}))
};
const jsonSize = JSON.stringify(originalData).length;
console.log(`Original JSON size: ${(jsonSize / 1024).toFixed(2)} KB`);
// With compression, typical reduction: 60-80%
// Compressed size: ~8-16 KB (from ~40 KB)
Connection Pooling
Connection pooling manages WebSocket connections efficiently, reusing connections and preventing resource exhaustion.
// WebSocket connection pool
class WebSocketPool {
constructor(url, poolSize = 5) {
this.url = url;
this.poolSize = poolSize;
this.connections = [];
this.availableConnections = [];
this.pendingRequests = [];
this.initialize();
}
initialize() {
for (let i = 0; i < this.poolSize; i++) {
this.createConnection();
}
}
createConnection() {
const ws = new WebSocket(this.url);
ws.onopen = () => {
this.availableConnections.push(ws);
this.processQueue();
};
ws.onclose = () => {
// Remove from available connections
const index = this.availableConnections.indexOf(ws);
if (index > -1) {
this.availableConnections.splice(index, 1);
}
// Recreate connection
setTimeout(() => this.createConnection(), 1000);
};
this.connections.push(ws);
}
async send(data) {
return new Promise((resolve, reject) => {
const request = { data, resolve, reject };
this.pendingRequests.push(request);
this.processQueue();
});
}
processQueue() {
while (this.pendingRequests.length > 0 &&
this.availableConnections.length > 0) {
const request = this.pendingRequests.shift();
const ws = this.availableConnections.shift();
ws.send(JSON.stringify(request.data));
// Return connection to pool after a delay
setTimeout(() => {
this.availableConnections.push(ws);
request.resolve();
}, 100);
}
}
close() {
this.connections.forEach(ws => ws.close());
}
}
// Usage
const pool = new WebSocketPool('ws://localhost:8080', 10);
for (let i = 0; i < 100; i++) {
pool.send({ message: `Message ${i}` });
}
Memory Management
Proper memory management prevents memory leaks and ensures stable long-running connections.
// Memory-efficient message handling
class OptimizedSocketServer {
constructor() {
this.connections = new Map(); // Use Map for O(1) operations
this.messageBuffer = new Map();
this.MAX_BUFFER_SIZE = 1000;
this.BUFFER_CLEANUP_INTERVAL = 60000; // 1 minute
// Periodic cleanup
setInterval(() => this.cleanupBuffers(), this.BUFFER_CLEANUP_INTERVAL);
}
addConnection(socket) {
this.connections.set(socket.id, {
socket,
joinedAt: Date.now(),
messageCount: 0,
lastActivity: Date.now()
});
// Setup listeners with proper cleanup
const messageHandler = this.handleMessage.bind(this, socket.id);
const disconnectHandler = this.handleDisconnect.bind(this, socket.id);
socket.on('message', messageHandler);
socket.on('disconnect', disconnectHandler);
// Store handlers for cleanup
socket._handlers = { messageHandler, disconnectHandler };
}
handleMessage(socketId, data) {
const connection = this.connections.get(socketId);
if (!connection) return;
connection.messageCount++;
connection.lastActivity = Date.now();
// Use circular buffer for message history
let buffer = this.messageBuffer.get(socketId);
if (!buffer) {
buffer = [];
this.messageBuffer.set(socketId, buffer);
}
buffer.push({
data,
timestamp: Date.now()
});
// Keep only recent messages
if (buffer.length > this.MAX_BUFFER_SIZE) {
buffer.shift();
}
}
handleDisconnect(socketId) {
const connection = this.connections.get(socketId);
if (!connection) return;
// Remove event listeners
const socket = connection.socket;
if (socket._handlers) {
socket.removeListener('message', socket._handlers.messageHandler);
socket.removeListener('disconnect', socket._handlers.disconnectHandler);
delete socket._handlers;
}
// Clean up data structures
this.connections.delete(socketId);
this.messageBuffer.delete(socketId);
}
cleanupBuffers() {
const now = Date.now();
const INACTIVE_THRESHOLD = 300000; // 5 minutes
for (const [socketId, connection] of this.connections.entries()) {
if (now - connection.lastActivity > INACTIVE_THRESHOLD) {
// Clear inactive buffers
this.messageBuffer.delete(socketId);
}
}
// Force garbage collection hint
if (global.gc) {
global.gc();
}
}
getMemoryUsage() {
const usage = process.memoryUsage();
return {
heapUsed: (usage.heapUsed / 1024 / 1024).toFixed(2) + ' MB',
heapTotal: (usage.heapTotal / 1024 / 1024).toFixed(2) + ' MB',
connections: this.connections.size,
bufferedMessages: Array.from(this.messageBuffer.values())
.reduce((sum, buf) => sum + buf.length, 0)
};
}
}
Memory Optimization Tips:
- Use WeakMap for caching when possible
- Implement circular buffers for message history
- Remove event listeners on disconnect
- Set limits on stored data per connection
- Use streaming for large data transfers
- Monitor memory usage with process.memoryUsage()
Efficient Serialization
Choosing the right serialization format significantly impacts performance.
// Serialization comparison
const data = {
userId: 12345,
timestamp: Date.now(),
position: { x: 100.5, y: 200.3, z: 50.1 },
health: 85,
inventory: [1, 2, 3, 4, 5]
};
// 1. JSON (Baseline)
const jsonStr = JSON.stringify(data);
console.log('JSON size:', jsonStr.length); // ~120 bytes
// 2. MessagePack (Binary JSON)
const msgpack = require('msgpack-lite');
const msgpackBuffer = msgpack.encode(data);
console.log('MessagePack size:', msgpackBuffer.length); // ~60 bytes (50% smaller)
// 3. Protocol Buffers (define schema)
const protobuf = require('protobufjs');
// Load .proto schema
const root = protobuf.loadSync('game.proto');
const GameState = root.lookupType('GameState');
// Encode
const message = GameState.create(data);
const buffer = GameState.encode(message).finish();
console.log('Protobuf size:', buffer.length); // ~40 bytes (67% smaller)
// 4. Custom binary protocol (most efficient for specific use cases)
function encodeGameState(state) {
const buffer = new ArrayBuffer(28);
const view = new DataView(buffer);
view.setUint32(0, state.userId);
view.setFloat64(4, state.timestamp);
view.setFloat32(12, state.position.x);
view.setFloat32(16, state.position.y);
view.setFloat32(20, state.position.z);
view.setUint8(24, state.health);
// ... encode inventory
return buffer;
}
const customBuffer = encodeGameState(data);
console.log('Custom binary size:', customBuffer.byteLength); // ~28 bytes
// Using MessagePack with Socket.io
const io = require('socket.io')(3000);
const msgpack = require('msgpack-lite');
// Server-side
io.on('connection', (socket) => {
socket.on('game-update', (buffer) => {
const data = msgpack.decode(new Uint8Array(buffer));
// Process game update
// Broadcast to others
const encoded = msgpack.encode(data);
socket.broadcast.emit('game-update', encoded);
});
});
// Client-side
const socket = io();
function sendGameUpdate(state) {
const encoded = msgpack.encode(state);
socket.emit('game-update', encoded.buffer);
}
socket.on('game-update', (buffer) => {
const state = msgpack.decode(new Uint8Array(buffer));
updateGameState(state);
});
Monitoring Connection Count
Track and optimize connection metrics for better resource management.
// Comprehensive connection monitoring
class ConnectionMonitor {
constructor(io) {
this.io = io;
this.metrics = {
totalConnections: 0,
activeConnections: 0,
peakConnections: 0,
connectionsByRoom: new Map(),
bandwidthUsage: { sent: 0, received: 0 },
messageCount: 0,
errorCount: 0
};
this.setupMonitoring();
this.startReporting();
}
setupMonitoring() {
this.io.on('connection', (socket) => {
this.metrics.totalConnections++;
this.metrics.activeConnections++;
if (this.metrics.activeConnections > this.metrics.peakConnections) {
this.metrics.peakConnections = this.metrics.activeConnections;
}
// Monitor messages
socket.use((packet, next) => {
this.metrics.messageCount++;
// Estimate bandwidth
const size = JSON.stringify(packet).length;
this.metrics.bandwidthUsage.received += size;
next();
});
// Track room memberships
socket.on('join-room', (roomId) => {
const count = this.metrics.connectionsByRoom.get(roomId) || 0;
this.metrics.connectionsByRoom.set(roomId, count + 1);
});
socket.on('leave-room', (roomId) => {
const count = this.metrics.connectionsByRoom.get(roomId) || 0;
this.metrics.connectionsByRoom.set(roomId, Math.max(0, count - 1));
});
socket.on('error', () => {
this.metrics.errorCount++;
});
socket.on('disconnect', () => {
this.metrics.activeConnections--;
});
});
}
startReporting() {
setInterval(() => {
const report = this.generateReport();
console.log('=== WebSocket Metrics ===\");
console.log(report);
// Reset counters
this.metrics.messageCount = 0;
this.metrics.bandwidthUsage = { sent: 0, received: 0 };
}, 60000); // Every minute
}
generateReport() {
const memUsage = process.memoryUsage();
return {
connections: {
active: this.metrics.activeConnections,
total: this.metrics.totalConnections,
peak: this.metrics.peakConnections
},
rooms: Object.fromEntries(this.metrics.connectionsByRoom),
performance: {
messagesPerMinute: this.metrics.messageCount,
bandwidthKBps: {
sent: (this.metrics.bandwidthUsage.sent / 1024 / 60).toFixed(2),
received: (this.metrics.bandwidthUsage.received / 1024 / 60).toFixed(2)
},
errors: this.metrics.errorCount
},
memory: {
heapUsed: (memUsage.heapUsed / 1024 / 1024).toFixed(2) + ' MB',
heapTotal: (memUsage.heapTotal / 1024 / 1024).toFixed(2) + ' MB'
}
};
}
getMetrics() {
return this.metrics;
}
}
// Usage
const monitor = new ConnectionMonitor(io);
// Expose metrics endpoint
app.get('/metrics', (req, res) => {
res.json(monitor.generateReport());
});
Backpressure Handling
Handle backpressure when sending data faster than clients can receive it.
// Backpressure-aware sending
class BackpressureSocket {
constructor(socket) {
this.socket = socket;
this.queue = [];
this.sending = false;
this.maxQueueSize = 100;
}
async send(data) {
return new Promise((resolve, reject) => {
if (this.queue.length >= this.maxQueueSize) {
reject(new Error('Send queue full'));
return;
}
this.queue.push({ data, resolve, reject });
this.processQueue();
});
}
async processQueue() {
if (this.sending || this.queue.length === 0) {
return;
}
this.sending = true;
while (this.queue.length > 0) {
const item = this.queue.shift();
try {
// Check if socket buffer is full
if (this.socket.bufferedAmount > 1024 * 1024) { // 1MB
// Wait for buffer to drain
await this.waitForDrain();
}
this.socket.send(JSON.stringify(item.data));
item.resolve();
} catch (error) {
item.reject(error);
}
}
this.sending = false;
}
waitForDrain() {
return new Promise((resolve) => {
const checkBuffer = () => {
if (this.socket.bufferedAmount < 512 * 1024) { // 512KB
resolve();
} else {
setTimeout(checkBuffer, 100);
}
};
checkBuffer();
});
}
}
// Usage
const bpSocket = new BackpressureSocket(ws);
for (let i = 0; i < 1000; i++) {
try {
await bpSocket.send({ message: `Data ${i}` });
} catch (error) {
console.error('Failed to send:', error);
break;
}
}
Practice Exercise:
- Implement binary data transfer for a game that sends player positions 60 times per second
- Create a connection pool that efficiently manages 100+ concurrent WebSocket connections
- Build a monitoring dashboard that displays real-time connection metrics and bandwidth usage
- Optimize a chat application by implementing MessagePack serialization and compression
- Implement backpressure handling for a data streaming application