Real-Time Chat UI — WebSocket, Smart Scroll & XSS
Chat mein naya msg aaya toh upar padhne wale user ko neeche mat kheencho! Smart scroll lagao, DocumentFragment use karo, aur XSS se bachein.
HTTP is request-response — the client asks, the server answers, and the connection closes. Chat needs real-time push from the server. When someone sends a message, your app must receive it instantly, without asking.
WebSocket solves this. new WebSocket(url) creates a persistent, bi-directional connection. Both client and server can send messages at any time — no waiting for requests.
WebSocket events:
1. open — Connection established, ready to send/receive.
2. message — Data received from the server.
3. close — Connection closed (network issue, server shutdown).
4. error — Something went wrong.
const socket = new WebSocket("wss://chat.example.com/ws");
socket.addEventListener("open", () => {
console.log("Connected to chat server!");
socket.send(JSON.stringify({ type: "join", room: "general" }));
});
socket.addEventListener("message", (event) => {
// event.data is a string, parse it
const msg = JSON.parse(event.data);
if (msg.type === "chat") {
appendMessage(msg.user, msg.text, msg.timestamp);
}
});
socket.addEventListener("close", () => {
console.log("Disconnected. Attempting reconnect...");
setTimeout(connectToChat, 3000); // Basic reconnect logic
});
In chat, messages arrive rapidly. Calling innerHTML += on every message is extremely slow — it destroys and recreates all existing DOM nodes. Instead, use document.createElement and appendChild for incremental updates.
For multiple messages at once (e.g., loading chat history), use DocumentFragment to batch insert. A fragment lives in memory — appending it to the DOM triggers only one reflow instead of one per message.
const chatBox = document.getElementById("chat-messages");
// Single message (efficient DOM append)
function appendMessage(user, text, time) {
const div = document.createElement("div");
div.className = "message";
const strong = document.createElement("strong");
strong.textContent = user;
const timeSpan = document.createElement("span");
timeSpan.className = "time";
timeSpan.textContent = formatTime(time);
const p = document.createElement("p");
p.textContent = text;
div.appendChild(strong);
div.appendChild(timeSpan);
div.appendChild(p);
chatBox.appendChild(div);
smartScroll();
}
// Batch loading history (DocumentFragment)
function loadHistory(messages) {
const fragment = document.createDocumentFragment();
messages.forEach(msg => {
const div = document.createElement("div");
div.className = "message";
const strong = document.createElement("strong");
strong.textContent = msg.user;
const p = document.createElement("p");
p.textContent = msg.text;
div.appendChild(strong);
div.appendChild(p);
fragment.appendChild(div);
});
chatBox.appendChild(fragment); // ONE DOM UPDATE for all messages!
}
You should auto-scroll to the bottom ONLY if the user is already at the bottom. If the user has scrolled up to read old messages, DO NOT force scroll down when new messages arrive — that is a terrible UX.
Formula: chatBox.scrollHeight - chatBox.scrollTop <= chatBox.clientHeight + tolerance
Always call scroll logic AFTER appending the new message, so scrollHeight has already been updated.
function isUserAtBottom() {
// 50px tolerance - consider "at bottom" if within 50px
const tolerance = 50;
return chatBox.scrollHeight - chatBox.scrollTop
<= chatBox.clientHeight + tolerance;
}
function smartScroll() {
if (isUserAtBottom()) {
// User is at bottom, auto-scroll to new message
chatBox.scrollTop = chatBox.scrollHeight;
} else {
// User is reading history, do NOT scroll
showNewMessageBadge();
}
}
// On receiving message
socket.addEventListener("message", (event) => {
const msg = JSON.parse(event.data);
appendMessage(msg.user, msg.text, msg.time);
// appendMessage already calls smartScroll()
});
scrollHeight - clientHeight. They might be 3 pixels up. Without tolerance, auto-scroll would break for anyone not pixel-perfect at the bottom. The 50px tolerance means "if you are within 50px of the bottom, you are close enough — we will scroll you down."Chat apps show relative time — "just now", "5m ago", "Yesterday". Nobody wants to see "2024-01-15T14:32:00.000Z" in their chat window.
Calculate the difference between Date.now() and the message timestamp. Convert milliseconds into seconds, minutes, hours, or days. Update relative times periodically using setInterval.
function formatTime(timestamp) {
const now = Date.now();
const diff = now - new Date(timestamp).getTime();
const seconds = Math.floor(diff / 1000);
if (seconds < 10) return "just now";
if (seconds < 60) return seconds + "s ago";
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return minutes + "m ago";
const hours = Math.floor(minutes / 60);
if (hours < 24) return hours + "h ago";
const days = Math.floor(hours / 24);
if (days < 7) return days + "d ago";
return new Date(timestamp).toLocaleDateString();
}
// Update timestamps every minute
setInterval(() => {
document.querySelectorAll(".time").forEach(el => {
el.textContent = formatTime(el.dataset.timestamp);
});
}, 60000);
setInterval every 60 seconds updates all visible timestamps. Since we only change textContent (not innerHTML), this is extremely lightweight — no reflow, just a repaint.Attach one listener to the form for sending messages. When the user submits, grab the input value, send it via WebSocket, and clear the input.
NEVER trust user input. A user might type <img src=x onerror=alert(1)>. If you insert that via innerHTML, the script executes — that is XSS (Cross-Site Scripting).
Always escape HTML entities before rendering text. The simplest way: use textContent instead of innerHTML, or run text through an escape function.
const form = document.getElementById("chat-form");
const input = document.getElementById("chat-input");
form.addEventListener("submit", (e) => {
e.preventDefault();
const text = input.value.trim();
if (!text) return;
// Send via WebSocket
socket.send(JSON.stringify({ type: "chat", text: text }));
input.value = "";
});
// Crucial: Escape HTML to prevent XSS
function escapeHTML(str) {
const div = document.createElement("div");
div.textContent = str; // textContent escapes automatically
return div.innerHTML;
}
innerHTML directly with user-generated text in a chat app. Even if you trust your users, someone might copy-paste a malicious script. Always run text through an escape function or use element.textContent = text to ensure the browser treats it as plain text, not executable HTML.Lo kar liya — Key Points:
- ✅ Use
WebSocketfor real-time, bi-directional communication, parsing received JSON strings withJSON.parse - ✅ Append messages using
document.createElementandappendChildinstead ofinnerHTML +=to avoid destroying the DOM - ✅ Use
DocumentFragmentwhen batch-loading chat history to insert multiple nodes with a single reflow - ✅ Implement smart auto-scroll: only scroll to the bottom if the user is already within a tolerance zone of the bottom edge
- ✅ Always escape HTML entities in user messages using
textContentor an escape function to prevent XSS attacks - ✅ Format timestamps relatively ("5m ago") and update them periodically with
setInterval
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login