1. Polling là gì?

Polling là kỹ thuật client định kỳ gửi request đến server để kiểm tra dữ liệu mới. Có hai loại polling chính:

1.1 Short Polling

async function shortPolling() {
  while (true) {
    try {
      const response = await fetch("/api/updates");
      const data = await response.json();
      processData(data);
    } catch (error) {
      console.error("Polling error:", error);
    }

    // Đợi 5 giây trước khi gửi request tiếp theo
    await new Promise((resolve) => setTimeout(resolve, 5000));
  }
}

function processData(data) {
  console.log("New data:", data);
}

1.2 Long Polling

interface UpdateResponse {
  data: any;
  timestamp: number;
}

class LongPollingClient {
  private lastTimestamp: number = 0;
  private endpoint: string;
  private timeout: number;

  constructor(endpoint: string, timeout: number = 30000) {
    this.endpoint = endpoint;
    this.timeout = timeout;
  }

  async startPolling() {
    while (true) {
      try {
        const response = await fetch(
          `${this.endpoint}?lastTimestamp=${this.lastTimestamp}`,
          {
            signal: AbortSignal.timeout(this.timeout),
          }
        );

        if (response.ok) {
          const data: UpdateResponse = await response.json();
          this.lastTimestamp = data.timestamp;
          this.handleUpdate(data);
        }
      } catch (error) {
        if (error instanceof DOMException && error.name === "TimeoutError") {
          continue; // Restart polling on timeout
        }
        console.error("Long polling error:", error);
        await new Promise((resolve) => setTimeout(resolve, 1000));
      }
    }
  }

  private handleUpdate(data: UpdateResponse) {
    console.log("Received update:", data);
  }
}

// Usage
const client = new LongPollingClient("/api/updates");
client.startPolling();

2. WebSockets là gì?

WebSocket là giao thức cho phép giao tiếp hai chiều giữa client và server qua một kết nối duy nhất.

2.1 Basic WebSocket Implementation

class WebSocketClient {
  private ws: WebSocket;
  private reconnectAttempts: number = 0;
  private maxReconnectAttempts: number = 5;
  private reconnectDelay: number = 1000;

  constructor(private url: string) {
    this.connect();
  }

  private connect() {
    this.ws = new WebSocket(this.url);

    this.ws.onopen = () => {
      console.log("Connected to server");
      this.reconnectAttempts = 0;
    };

    this.ws.onmessage = (event) => {
      this.handleMessage(event.data);
    };

    this.ws.onclose = () => {
      console.log("Connection closed");
      this.handleReconnect();
    };

    this.ws.onerror = (error) => {
      console.error("WebSocket error:", error);
    };
  }

  private handleReconnect() {
    if (this.reconnectAttempts < this.maxReconnectAttempts) {
      this.reconnectAttempts++;
      console.log(`Reconnecting... Attempt ${this.reconnectAttempts}`);

      setTimeout(() => {
        this.connect();
      }, this.reconnectDelay * this.reconnectAttempts);
    } else {
      console.error("Max reconnection attempts reached");
    }
  }

  private handleMessage(data: string) {
    try {
      const parsedData = JSON.parse(data);
      console.log("Received:", parsedData);
    } catch (error) {
      console.error("Error parsing message:", error);
    }
  }

  public send(data: any) {
    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(data));
    } else {
      console.error("Connection not open");
    }
  }

  public close() {
    this.ws.close();
  }
}

2.2 Type-safe WebSocket với TypeScript

interface WebSocketMessage<T = any> {
  type: string;
  payload: T;
}

interface ChatMessage {
  id: string;
  user: string;
  content: string;
  timestamp: number;
}

class TypedWebSocketClient {
  private ws: WebSocket;
  private messageHandlers: Map<string, (payload: any) => void>;

  constructor(url: string) {
    this.ws = new WebSocket(url);
    this.messageHandlers = new Map();
    this.setupWebSocket();
  }

  private setupWebSocket() {
    this.ws.onmessage = (event) => {
      try {
        const message: WebSocketMessage = JSON.parse(event.data);
        const handler = this.messageHandlers.get(message.type);

        if (handler) {
          handler(message.payload);
        }
      } catch (error) {
        console.error("Error handling message:", error);
      }
    };
  }

  public on<T>(type: string, handler: (payload: T) => void) {
    this.messageHandlers.set(type, handler);
  }

  public send<T>(type: string, payload: T) {
    const message: WebSocketMessage<T> = { type, payload };
    this.ws.send(JSON.stringify(message));
  }
}

// Usage
const chat = new TypedWebSocketClient("ws://chat.server");

chat.on<ChatMessage>("message", (message) => {
  console.log(`${message.user}: ${message.content}`);
});

chat.send("message", {
  id: crypto.randomUUID(),
  user: "John",
  content: "Hello!",
  timestamp: Date.now(),
});

3. Ví dụ Thực Tế: Real-time Chat Application

3.1 Polling Version

interface ChatState {
  messages: ChatMessage[];
  users: string[];
  lastUpdate: number;
}

class PollingChatClient {
  private state: ChatState = {
    messages: [],
    users: [],
    lastUpdate: 0,
  };

  constructor(private apiUrl: string) {}

  async start() {
    this.pollMessages();
    this.pollUsers();
  }

  private async pollMessages() {
    while (true) {
      try {
        const response = await fetch(
          `${this.apiUrl}/messages?since=${this.state.lastUpdate}`
        );
        const data = await response.json();

        if (data.messages.length > 0) {
          this.state.messages.push(...data.messages);
          this.state.lastUpdate = data.timestamp;
          this.renderMessages();
        }
      } catch (error) {
        console.error("Error polling messages:", error);
      }

      await new Promise((resolve) => setTimeout(resolve, 1000));
    }
  }

  private async pollUsers() {
    while (true) {
      try {
        const response = await fetch(`${this.apiUrl}/users`);
        const users = await response.json();

        if (JSON.stringify(users) !== JSON.stringify(this.state.users)) {
          this.state.users = users;
          this.renderUsers();
        }
      } catch (error) {
        console.error("Error polling users:", error);
      }

      await new Promise((resolve) => setTimeout(resolve, 5000));
    }
  }

  private renderMessages() {
    // Update UI with new messages
  }

  private renderUsers() {
    // Update UI with user list
  }
}

3.2 WebSocket Version

interface ChatEvent {
  type: "message" | "user_joined" | "user_left";
  payload: any;
}

class WebSocketChatClient {
  private ws: WebSocket;
  private state: ChatState = {
    messages: [],
    users: [],
    lastUpdate: 0,
  };

  constructor(private wsUrl: string) {
    this.connect();
  }

  private connect() {
    this.ws = new WebSocket(this.wsUrl);

    this.ws.onmessage = (event) => {
      const chatEvent: ChatEvent = JSON.parse(event.data);

      switch (chatEvent.type) {
        case "message":
          this.handleNewMessage(chatEvent.payload);
          break;
        case "user_joined":
          this.handleUserJoined(chatEvent.payload);
          break;
        case "user_left":
          this.handleUserLeft(chatEvent.payload);
          break;
      }
    };

    this.ws.onclose = () => {
      console.log("Connection lost, reconnecting...");
      setTimeout(() => this.connect(), 1000);
    };
  }

  private handleNewMessage(message: ChatMessage) {
    this.state.messages.push(message);
    this.state.lastUpdate = Date.now();
    this.renderMessages();
  }

  private handleUserJoined(user: string) {
    if (!this.state.users.includes(user)) {
      this.state.users.push(user);
      this.renderUsers();
    }
  }

  private handleUserLeft(user: string) {
    const index = this.state.users.indexOf(user);
    if (index !== -1) {
      this.state.users.splice(index, 1);
      this.renderUsers();
    }
  }

  public sendMessage(content: string) {
    const message: ChatMessage = {
      id: crypto.randomUUID(),
      user: "current_user",
      content,
      timestamp: Date.now(),
    };

    this.ws.send(
      JSON.stringify({
        type: "message",
        payload: message,
      })
    );
  }

  private renderMessages() {
    // Update UI with new messages
  }

  private renderUsers() {
    // Update UI with user list
  }
}

3.5 Server-Sent Events (SSE), nửa đường giữa Polling và WebSocket

Giữa “dội liên tục” và “kết nối full-duplex”, SSE (EventSource) là lựa chọn thường bị bỏ quên: server đẩy một chiều xuống client qua một HTTP response mở kéo dài. Ưu thế:

  • Chạy trên HTTP/1.1, HTTP/2, HTTP/3 nguyên thuỷ, qua được mọi proxy, CDN, load balancer.
  • Auto-reconnectLast-Event-ID resume built-in trong browser.
  • Không cần thay đổi giao thức (không upgrade như WebSocket).
  • Gọn hơn WebSocket khi chỉ cần server → client (notification, log tail, AI streaming token, progress bar).
// Client
const source = new EventSource("/api/stream", { withCredentials: true });

source.addEventListener("message", (e) => {
  const data = JSON.parse(e.data);
  console.log("Event:", data);
});

source.addEventListener("ping", (e) => console.log("heartbeat"));

source.onerror = () => {
  // Browser tự reconnect, chỉ cần đóng khi muốn stop hẳn
  console.warn("SSE disconnected, browser will retry");
};

// Huỷ khi rời trang
source.close();
// Server (Node.js / Express)
app.get("/api/stream", (req, res) => {
  res.set({
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
    "X-Accel-Buffering": "no", // bắt nginx không buffer
  });
  res.flushHeaders();

  let id = 0;
  const timer = setInterval(() => {
    res.write(`id: ${++id}\n`);
    res.write(`event: message\n`);
    res.write(`data: ${JSON.stringify({ time: Date.now() })}\n\n`);
  }, 1000);

  req.on("close", () => clearInterval(timer));
});

Chú ý giới hạn của SSE:

  • Chỉ một chiều (client muốn gửi → dùng fetch POST riêng).
  • HTTP/1.1 có giới hạn 6 kết nối đồng thời per domain → SSE mở liên tục dễ “ăn slot”. Với HTTP/2/3 thì không (multiplexing).

3.6 Bảng so sánh 4 kỹ thuật thường dùng cho “real-time”

Tiêu chíShort pollingLong pollingSSE (EventSource)WebSocket
ChiềuClient pullClient pullServer → ClientFull duplex
Giao thứcHTTP request bình thườngHTTP request dàiHTTP streamingUpgrade → ws/wss
Proxy / CDN friendlyTốtVừaTốtPhải cấu hình
Auto-reconnectTự dùng setIntervalTự codeBuilt-in browserTự code (hoặc lib)
Độ trễCao (= chu kỳ poll)Trung bìnhThấpRất thấp
Server cost (N clients)Cao nhấtTrung bìnhMột kết nối mởMột kết nối mở
BinaryKhôngKhôngKhông (text/UTF-8)
Client → ServerTách requestTách requestTách requestCùng kênh
Use case phù hợpDashboard thỉnh thoảngNotification nhẹAI token streaming, progress, log tail, notificationChat, multiplayer, collab editor, WebRTC signaling

103 Early Hints (RFC 8297) đôi khi bị nhầm với “server push”, nó không phải stream data; đó là HTTP 1xx informational để gợi ý <link rel="preload">/<link rel="preconnect"> trong khi server vẫn đang compute response chính. Hữu ích cho page load performance, không phải real-time.

3.7 Heuristic chọn nhanh

  1. Server chỉ đẩy, client ít gửi (dashboard, monitor, AI stream, notification) → SSE.
  2. Hai chiều thường xuyên (chat, game, collab) → WebSocket.
  3. Dữ liệu thay đổi chậm (>= 30s) hoặc infra đơn giản → Short polling + cache headers.
  4. Client cần cập nhật gần real-time nhưng hạ tầng không cho WebSocket (corporate proxy, legacy LB) → Long polling hoặc SSE.
  5. Tải tài nguyên nhanh khi server đang compute103 Early Hints (không liên quan real-time, chỉ giảm TTFB cho preload).

4. So sánh Polling và WebSockets

4.1 Polling

Ưu điểm:

  • Dễ triển khai
  • Hoạt động với mọi browser
  • Không cần thay đổi server infrastructure
  • Phù hợp với dữ liệu cập nhật không thường xuyên

Nhược điểm:

  • Tốn bandwidth
  • Độ trễ cao
  • Server load lớn
  • Không real-time thực sự

4.2 WebSockets

Ưu điểm:

  • Real-time thực sự
  • Hiệu quả về bandwidth
  • Độ trễ thấp
  • Giao tiếp hai chiều

Nhược điểm:

  • Phức tạp hơn để triển khai
  • Cần server hỗ trợ WebSocket
  • Có thể gặp vấn đề với firewalls
  • Cần xử lý reconnection

5. Khi nào nên sử dụng?

5.1 Sử dụng Polling khi:

  • Dữ liệu cập nhật không thường xuyên
  • Không yêu cầu real-time tuyệt đối
  • Server infrastructure đơn giản
  • Cần hỗ trợ nhiều loại client

5.2 Sử dụng WebSockets khi:

  • Cần real-time thực sự
  • Dữ liệu cập nhật thường xuyên
  • Cần giao tiếp hai chiều
  • Bandwidth là vấn đề quan trọng

6. Kết luận

Polling và WebSockets đều có vai trò riêng trong real-time communication. Việc lựa chọn phụ thuộc vào yêu cầu cụ thể của ứng dụng, infrastructure hiện có và trade-offs có thể chấp nhận được.