Streaming dữ liệu Real-time với SSE trong Node.js: Đừng dùng WebSocket cho mọi thứ

Development tutorial - IT technology blog
Development tutorial - IT technology blog

Cơn đau đầu mang tên “Cập nhật dữ liệu thời gian thực”

Tuần trước, mình nhận task xây dựng một dashboard theo dõi log hệ thống. Yêu cầu nghe thì đơn giản: log server bắn ra đến đâu, trình duyệt admin phải hiện ngay đến đó với độ trễ dưới 100ms. Anh em làm nghề chắc không lạ gì bài toán này, nhất là khi làm thông báo (notifications), bảng giá chứng khoán, hay hot nhất hiện nay là streaming kết quả từ các model AI như ChatGPT.

Mới đầu, mình định dùng đại một công nghệ nào đó quen tay cho xong. Nhưng khi ngồi tính toán kỹ lượng traffic, mình nhận ra nếu chọn sai công nghệ ngay từ đầu, server sẽ sớm “ngộp thở” khi lượng user tăng lên.

Tại sao những cách làm cũ lại khiến bạn mất ngủ?

Đầu tiên là Short Polling. Cứ mỗi 1-2 giây, client lại gửi một request HTTP hỏi: “Có gì mới không?”. Cách này cực kỳ đốt tài nguyên. Ngay cả khi hệ thống im lìm, server vẫn phải gồng mình tiếp nhận request, truy vấn database rồi trả về một mảng rỗng vô nghĩa. Thử tưởng tượng có 1.000 user online đồng thời, mỗi giây server phải gánh 1.000 request chỉ để nói “không có gì”, CPU nào trụ cho nổi?

Tiếp theo là WebSocket. Đây là “ông trùm” trong làng real-time nhờ khả năng giao tiếp hai chiều. Tuy nhiên, với nhu cầu chỉ cần đẩy dữ liệu từ server xuống client, WebSocket giống như dùng xe container để đi giao một hộp cơm vậy. Bạn phải cài thêm thư viện (như Socket.io), lo cấu hình handshake, xử lý rớt mạng, và đau đầu nhất là cấu hình header Upgrade khi chạy sau Nginx hoặc Load Balancer.

Server-Sent Events (SSE): Người hùng thầm lặng

Sau khi cân nhắc, mình chọn Server-Sent Events (SSE). Đây là tiêu chuẩn HTML5 cho phép server chủ động đẩy dữ liệu xuống client qua một kết nối HTTP duy nhất được giữ mở (persistent connection).

Tại sao SSE lại là lựa chọn thông minh hơn?

  • HTTP Thuần túy: Không cần giao thức phức tạp, chạy mượt mà trên các cổng 80/443 tiêu chuẩn.
  • Siêu nhẹ: Overhead của mỗi tin nhắn cực thấp. Một request HTTP thông thường tốn khoảng 500 bytes header, còn SSE gửi dữ liệu gần như trực tiếp.
  • Cơ chế tự hồi phục: Trình duyệt sẽ tự động kết nối lại nếu mạng chập chờn mà không cần bạn viết thêm dòng code nào.
  • Triển khai thần tốc: Frontend chỉ tốn đúng 3 dòng code để bắt đầu nhận dữ liệu.

Triển khai SSE với Node.js: Gọn gàng và hiệu quả

Dưới đây là cách mình dựng một server Express đơn giản để đẩy timestamp xuống client mỗi 2 giây.

Bước 1: Setup Backend với Express

Bí mật nằm ở việc thiết lập đúng các HTTP Header để trình duyệt hiểu đây là một luồng dữ liệu liên tục.

const express = require('express');
const app = express();
const PORT = 3000;

app.get('/events', (req, res) => {
    // Thiết lập header bắt buộc cho SSE
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');

    res.write('data: Đã kết nối thành công!\n\n');

    const intervalId = setInterval(() => {
        const data = JSON.stringify({ 
            message: 'Log hệ thống mới', 
            timestamp: new Date().toLocaleTimeString() 
        });
        
        // Lưu ý: Phải có tiền tố "data: " và kết thúc bằng "\n\n"
        res.write(`data: ${data}\n\n`);
    }, 2000);

    // Dọn dẹp tài nguyên khi user tắt tab
    req.on('close', () => {
        clearInterval(intervalId);
        res.end();
        console.log('User đã rời đi, dừng interval.');
    });
});

app.listen(PORT, () => {
    console.log(`Server đang đợi ở http://localhost:${PORT}`);
});

Mẹo nhỏ: Khi streaming dữ liệu JSON phức tạp, mình thường copy payload vào toolcraft.app/vi/tools/developer/json-formatter để kiểm tra cấu trúc nhanh. Việc này giúp tránh lỗi parse JSON ở phía frontend do sai định dạng.

Bước 2: Phía Frontend chỉ cần “lắng nghe”

Bạn không cần cài thư viện NPM nào cả. API EventSource đã có sẵn trong mọi trình duyệt hiện đại.

const eventSource = new EventSource('/events');

eventSource.onmessage = (event) => {
    const data = JSON.parse(event.data);
    console.log('Data mới về:', data);
    
    const list = document.getElementById('logs');
    list.innerHTML += `<li>${data.timestamp}: ${data.message}</li>`;
};

eventSource.onerror = () => {
    console.log('Mất kết nối, trình duyệt đang thử lại...');
};

3 lưu ý “xương máu” để không bị ăn hành

Dù ngon bổ rẻ, SSE vẫn có những hạn chế mà bạn cần biết để tránh lỗi trên môi trường production:

  1. Giới hạn kết nối cũ: Với HTTP/1.1, trình duyệt chỉ cho phép mở tối đa 6 kết nối SSE trên mỗi domain. Nếu user mở quá 6 tab, các tab sau sẽ bị treo hoàn toàn. Hãy ưu tiên dùng HTTP/2 để nâng giới hạn này lên hàng trăm kết nối.
  2. Nginx Buffering: Nginx thường đợi gom đủ dữ liệu mới gửi về client. Điều này giết chết tính real-time. Hãy nhớ thêm header X-Accel-Buffering: no vào config để dữ liệu được đẩy đi ngay lập tức.
  3. Ký tự xuống dòng: Luôn kết thúc mỗi tin nhắn bằng hai ký tự \n\n. Thiếu một dấu xuống dòng cũng đủ khiến client đứng hình vì tưởng dữ liệu chưa truyền xong.

Lời kết

Sau khi thay thế Polling bằng SSE cho hệ thống dashboard, mức sử dụng CPU của server mình đã giảm từ 40% xuống chỉ còn 5%. Nếu bạn đang làm ứng dụng chỉ cần luồng dữ liệu một chiều như thông báo, cập nhật tỷ giá hay streaming AI, SSE chính là chân ái. Code sạch, nhẹ máy, lại cực kỳ dễ bảo trì. Chúc anh em áp dụng thành công!

Share: