Implementing Live Updates (Updates Without Page Reload) on a Website
Real-time updates without page reload — this isn't necessarily WebSocket. Technology choice depends on data directionality, update frequency, and acceptable infrastructure complexity.
Three Approaches and When to Apply Each
Server-Sent Events (SSE) — one-way stream from server, regular HTTP. Ideal for notifications, activity feeds, task progress. Built-in automatic browser reconnection.
WebSocket — two-way channel. Needed when client also sends real-time data (chat, games, collaborative editing).
Polling / Long Polling — HTTP requests at intervals or awaiting response. Simplest option, suitable for rare updates (every 30–60 seconds).
| Technology | Direction | Infrastructure | When |
|---|---|---|---|
| SSE | Server → Client | Any HTTP server | Notifications, feeds, statuses |
| WebSocket | Two-way | WS server | Chat, games, collaboration |
| Polling | Client → Server | Any | Rare updates, simplicity |
| Long Polling | Client ↔ Server | Any | Fallback for SSE |
Server-Sent Events: Implementation
SSE works over regular HTTP response with Content-Type: text/event-stream. Connection stays open, server pushes events:
// Node.js / Express
app.get('/api/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no'); // Important for nginx
const userId = req.user.id;
// Send initial state
res.write(`data: ${JSON.stringify({ type: 'init', unread: 5 })}\n\n`);
// Subscribe to events
const unsubscribe = eventBus.subscribe(userId, (event) => {
res.write(`event: ${event.type}\n`);
res.write(`data: ${JSON.stringify(event.payload)}\n`);
res.write(`id: ${event.id}\n\n`); // for Last-Event-ID
});
// Keepalive every 30 seconds
const heartbeat = setInterval(() => {
res.write(': heartbeat\n\n');
}, 30000);
req.on('close', () => {
clearInterval(heartbeat);
unsubscribe();
});
});
Client:
const evtSource = new EventSource('/api/events', {
withCredentials: true,
});
// Listen to named events
evtSource.addEventListener('notification', (e) => {
const data = JSON.parse(e.data);
showNotification(data);
});
evtSource.addEventListener('order-status', (e) => {
updateOrderStatus(JSON.parse(e.data));
});
// Generic handler
evtSource.onmessage = (e) => {
console.log('Default event:', e.data);
};
// Browser auto-reconnects
evtSource.onerror = (e) => {
console.log('SSE error, will reconnect...');
};
EventSource auto-reconnects on disconnect, sending Last-Event-ID — server can resend missed events.
WebSocket with Reconnection
Native WebSocket doesn't reconnect itself. Needs a wrapper:
class ReconnectingWebSocket {
constructor(url, protocols) {
this.url = url;
this.protocols = protocols;
this.reconnectDelay = 1000;
this.maxDelay = 30000;
this.listeners = new Map();
this.connect();
}
connect() {
this.ws = new WebSocket(this.url, this.protocols);
this.ws.onopen = () => {
this.reconnectDelay = 1000;
this.emit('open');
};
this.ws.onmessage = (e) => this.emit('message', JSON.parse(e.data));
this.ws.onclose = () => {
this.emit('close');
setTimeout(() => this.connect(), this.reconnectDelay);
this.reconnectDelay = Math.min(this.reconnectDelay * 1.5, this.maxDelay);
};
}
send(data) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
}
}
on(event, cb) {
if (!this.listeners.has(event)) this.listeners.set(event, []);
this.listeners.get(event).push(cb);
}
emit(event, data) {
this.listeners.get(event)?.forEach(cb => cb(data));
}
}
Or use ready-made: reconnecting-websocket, socket.io (with built-in polling fallback).
Update UI Without Flashing
Crude innerHTML replacement on each update creates visual artifacts. Two approaches:
Morphdom — DOM-diff without Virtual DOM:
import morphdom from 'morphdom';
ws.on('message', ({ type, html }) => {
if (type === 'update-block') {
const el = document.getElementById('notifications');
morphdom(el, `<div id="notifications">${html}</div>`, {
onBeforeElUpdated: (fromEl, toEl) => {
// Don't update elements during animation
if (fromEl.classList.contains('animating')) return false;
return true;
}
});
}
});
React / Vue state updates — if frontend is React, just update state:
ws.on('message', (data) => {
switch (data.type) {
case 'new-order':
setOrders(prev => [data.order, ...prev]);
break;
case 'order-updated':
setOrders(prev => prev.map(o => o.id === data.order.id ? data.order : o));
break;
case 'notification':
setNotifications(prev => [data.notification, ...prev.slice(0, 49)]);
break;
}
});
Broadcasting via Redis Pub/Sub
With multiple server instances — Redis Pub/Sub for broadcasting events to all connected clients:
// publisher.js
const redis = require('redis');
const publisher = redis.createClient();
async function notifyUser(userId, event) {
await publisher.publish(
`user:${userId}`,
JSON.stringify(event)
);
}
// subscriber.js (same process holding SSE/WS connections)
const subscriber = redis.createClient();
await subscriber.subscribe(`user:${userId}`, (message) => {
const event = JSON.parse(message);
sseConnections.get(userId)?.forEach(res => {
res.write(`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`);
});
});
Optimization: Batch Updates and Debounce
With high-frequency updates (prices, metrics), don't send each change separately:
// Server: event buffering
class UpdateBatcher {
constructor(flushInterval = 100) {
this.queue = new Map(); // userId -> events
setInterval(() => this.flush(), flushInterval);
}
queue(userId, event) {
if (!this.queue.has(userId)) this.queue.set(userId, []);
this.queue.get(userId).push(event);
}
flush() {
this.queue.forEach((events, userId) => {
if (events.length) {
sendBatch(userId, events);
this.queue.set(userId, []);
}
});
}
}
Timeline
SSE notifications (new orders, messages) — 1–2 days WebSocket with reconnection and React integration — 2–3 days Broadcasting via Redis Pub/Sub for multiple servers — plus 1–2 days Full real-time feed (activity, notifications, counters) — 4–6 days







