Chapter 10.6☕ 25 min read

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.

01🌐 WebSocket: Real-Time Connection

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
});
HTTP vs WebSocket: HTTP is like sending a letter — you write, they reply, done. WebSocket is like a phone call — both sides can talk anytime. The connection stays open until someone hangs up. This is why chat apps, live sports scores, and multiplayer games all use WebSocket.
02📦 Efficient DOM Updates: DocumentFragment

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!
}
Why DocumentFragment is fast: When you append 100 messages one by one, the browser recalculates layout 100 times. With DocumentFragment, all 100 nodes are collected in memory first, then appended in a single operation. The browser recalculates layout only once. This is the difference between 100 reflows and 1 reflow.
03🔄 Smart Auto-Scroll Logic

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()
});
The tolerance zone: A user is almost never at exactly 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."
04⏱️ Timestamp Formatting: "2 mins ago"

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);
Why update timestamps periodically: If a message says "1m ago" and 5 minutes pass, it should say "6m ago" — not stay frozen at "1m ago". A simple setInterval every 60 seconds updates all visible timestamps. Since we only change textContent (not innerHTML), this is extremely lightweight — no reflow, just a repaint.
05🛡️ Event Handling & XSS Prevention

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;
}
Never use 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 WebSocket for real-time, bi-directional communication, parsing received JSON strings with JSON.parse
  • ✅ Append messages using document.createElement and appendChild instead of innerHTML += to avoid destroying the DOM
  • ✅ Use DocumentFragment when 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 textContent or an escape function to prevent XSS attacks
  • ✅ Format timestamps relatively ("5m ago") and update them periodically with setInterval
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase