CSR – Client-Side Rendering
Bài 10 – Cơ chế Client-Side Rendering trong Next.js và khi nào nên dùng
1. Giới thiệu về Client-Side Rendering (CSR)
CSR (Client-Side Rendering) là cơ chế render UI hoàn toàn ở phía trình duyệt. Thay vì server trả về HTML đã render, trình duyệt sẽ nhận bundle JavaScript và tự dựng giao diện. Trong Next.js, CSR được sử dụng khi trang cần tương tác nhiều, dùng state phức tạp, hoặc không yêu cầu SEO.
2. Nội dung chính
2.1. CSR là gì và cách hoạt động của nó
Client-Side Rendering hoạt động theo cơ chế:
- Server trả về HTML trống với JavaScript bundle
- Trình duyệt tải và thực thi JavaScript
- JavaScript tạo và cập nhật DOM để hiển thị nội dung
- Các tương tác sau đó được xử lý hoàn toàn ở client
2.2. Khi nào sử dụng CSR thay vì SSR/SSG/ISR
CSR phù hợp khi:
- Trang yêu cầu tương tác nhiều (dashboard, admin panel)
- Không cần SEO (trang sau đăng nhập)
- Cần cập nhật realtime (chat, trading)
- State phức tạp với nhiều component con
2.3. use client và các hook React
Trong Next.js App Router, để sử dụng CSR cần khai báo "use client":
"use client";
import { useState, useEffect } from 'react';
export default function ClientComponent() {
const [data, setData] = useState(null);
useEffect(() => {
// Fetch data ở client
fetch('/api/data')
.then(res => res.json())
.then(setData);
}, []);
return <div>{data ? data.message : 'Loading...'}</div>;
}
2.4. CSR trong App Router và cách tách nhỏ component
Best practice là tách biệt server và client components:
// app/page.tsx - Server Component
import ClientWidget from './ClientWidget';
export default async function Page() {
const serverData = await fetchServerData();
return (
<div>
<h1>{serverData.title}</h1>
<ClientWidget initialData={serverData} />
</div>
);
}
// app/ClientWidget.tsx - Client Component
"use client";
import { useState } from 'react';
export default function ClientWidget({ initialData }) {
const [count, setCount] = useState(0);
return (
<div>
<p>{initialData.description}</p>
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
</div>
);
}
2.5. Tác động của CSR lên hiệu năng và bundle size
Cân nhắc khi sử dụng CSR:
- Tăng bundle size do cần JavaScript để render
- Ảnh hưởng SEO nếu không cấu hình đúng
- Có thể gây FOUC (Flash of Unstyled Content)
- Cần xử lý loading state và error boundary
3. Ví dụ thực tế
3.1. Dashboard Stats với CSR
// app/dashboard/stats/page.tsx – CSR cho phần UI có state
"use client";
import { useEffect, useState } from "react";
interface StatsData {
users: number;
revenue: number;
orders: number;
lastUpdated: string;
}
export default function Stats() {
const [stats, setStats] = useState<StatsData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch("/api/stats")
.then((res) => {
if (!res.ok) throw new Error("Failed to fetch stats");
return res.json();
})
.then((data) => {
setStats(data);
setLoading(false);
})
.catch((err) => {
setError(err.message);
setLoading(false);
});
}, []);
if (loading) return <div className="loading">Đang tải thống kê...</div>;
if (error) return <div className="error">Lỗi: {error}</div>;
if (!stats) return null;
return (
<div className="stats-container">
<h1>Thống kê theo thời gian thực</h1>
<div className="stats-grid">
<div className="stat-card">
<h3>Người dùng</h3>
<p className="stat-value">{stats.users.toLocaleString()}</p>
</div>
<div className="stat-card">
<h3>Doanh thu</h3>
<p className="stat-value">${stats.revenue.toLocaleString()}</p>
</div>
<div className="stat-card">
<h3>Đơn hàng</h3>
<p className="stat-value">{stats.orders.toLocaleString()}</p>
</div>
</div>
<p className="last-updated">
Cập nhật lần cuối: {new Date(stats.lastUpdated).toLocaleString()}
</p>
</div>
);
}
3.2. Counter Component đơn giản
// app/counter/page.tsx
"use client";
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div className="counter-container">
<h1>Counter Demo</h1>
<div className="counter-display">
<span className="count-value">{count}</span>
</div>
<div className="counter-controls">
<button
onClick={() => setCount(count - 1)}
className="btn-decrease"
>
Giảm
</button>
<button
onClick={() => setCount(0)}
className="btn-reset"
>
Reset
</button>
<button
onClick={() => setCount(count + 1)}
className="btn-increase"
>
Tăng
</button>
</div>
</div>
);
}
3.3. Real-time Chat Component
// app/chat/page.tsx
"use client";
import { useEffect, useState } from "react";
interface Message {
id: string;
user: string;
text: string;
timestamp: number;
}
export default function Chat() {
const [messages, setMessages] = useState<Message[]>([]);
const [inputText, setInputText] = useState("");
const [connected, setConnected] = useState(false);
useEffect(() => {
// Kết nối WebSocket hoặc polling
const eventSource = new EventSource("/api/chat/stream");
eventSource.onmessage = (event) => {
const newMessage = JSON.parse(event.data);
setMessages(prev => [...prev, newMessage]);
};
eventSource.onopen = () => setConnected(true);
eventSource.onerror = () => setConnected(false);
return () => eventSource.close();
}, []);
const sendMessage = async (e: React.FormEvent) => {
e.preventDefault();
if (!inputText.trim()) return;
await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: inputText }),
});
setInputText("");
};
return (
<div className="chat-container">
<h1>Chat Real-time</h1>
<div className="connection-status">
Trạng thái: {connected ? "✅ Đã kết nối" : "❌ Mất kết nối"}
</div>
<div className="messages-list">
{messages.map((msg) => (
<div key={msg.id} className="message">
<span className="message-user">{msg.user}:</span>
<span className="message-text">{msg.text}</span>
<span className="message-time">
{new Date(msg.timestamp).toLocaleTimeString()}
</span>
</div>
))}
</div>
<form onSubmit={sendMessage} className="chat-form">
<input
type="text"
value={inputText}
onChange={(e) => setInputText(e.target.value)}
placeholder="Nhập tin nhắn..."
className="chat-input"
/>
<button type="submit" className="send-button">
Gửi
</button>
</form>
</div>
);
}
4. Kiến thức trọng tâm
4.1. Khi nào nên dùng CSR
✅ Nên dùng CSR khi:
- Trang tương tác mạnh, yêu cầu cập nhật realtime
- Không quan trọng SEO (trang sau đăng nhập)
- Cần sử dụng browser APIs (localStorage, geolocation)
- State phức tạp với nhiều component con
❌ Không nên dùng CSR khi:
- Trang cần SEO tốt (landing page, blog)
- Nội dung tĩnh, ít thay đổi
- Cần performance tối đa cho lần đầu load
4.2. Best Practices cho Client Components
- Luôn khai báo “use client” ở đầu file
- Tách nhỏ components - chỉ client hóa phần cần thiết
- Xử lý loading states để tránh FOUC
- Sử dụng error boundaries cho xử lý lỗi graceful
- Tối ưu bundle size với dynamic imports khi cần
4.3. Cách tối ưu performance
// Tách client component nhỏ nhất có thể
"use client";
import { useState } from 'react';
// Chỉ client hóa phần cần tương tác
export default function InteractiveButton({ initialCount }) {
const [count, setCount] = useState(initialCount);
return (
<button onClick={() => setCount(count + 1)}>
Đã click {count} lần
</button>
);
}
5. Bài tập nhanh
5.1. Tạo trang /counter với nút tăng giảm số bằng CSR
"use client";
import { useState } from "react";
// TODO: Tạo component counter với:
// - Nút tăng/giảm số
// - Giới hạn từ 0 đến 100
// - Hiển thị số hiện tại
// - Thêm animation khi thay đổi
5.2. Tạo trang /chat fetch dữ liệu realtime từ API dùng useEffect
"use client";
import { useEffect, useState } from "react";
// TODO: Tạo chat component với:
// - Kết nối API /api/messages
// - Hiển thị danh sách tin nhắn
// - Form gửi tin nhắn mới
// - Tự động refresh mỗi 5 giây
5.3. Xác định một page có thể tách thành server component + client widget để tối ưu
Phân tích và tách:
- Phần header, footer → Server Component
- Phần tìm kiếm, lọc → Client Component
- Phần danh sách sản phẩm → Server Component
- Giỏ hàng mini → Client Component
6. Kết luận
CSR là chiến lược cần thiết cho các trang giàu tương tác, nhưng cần sử dụng hợp lý để tránh tăng bundle size và giảm hiệu năng. Nên kết hợp CSR cùng Server Components để đạt hiệu quả tối ưu.
Tóm tắt key points:
- CSR phù hợp cho trang tương tác, không cần SEO
- Luôn khai báo
"use client"cho Client Components - Tách nhỏ components để tối ưu performance
- Kết hợp với Server Components cho best of both worlds
- Xử lý loading states và error boundaries properly