Trung bình Next.js

Next.js App Router - Server Components vs Client Components, Xài Sao Cho Chuẩn

Server Component chỉ chạy trên server; Client Component mới hydrate ở trình duyệt. Coi Next.js App Router render hai loại này ra sao, và khi nào mới thiệt sự cần 'use client'.

14 Th07, 2026 4 phút 1,202 Lượt xem 2 Khối code
Sơ đồ
graph TD A[Browser Request] --> B[Next.js Server] B --> C[Render Server Components] C --> D[Fetch Data / DB / Secrets] D --> E[Generate HTML + RSC Payload] E --> F[Send Response to Browser] F --> G[Paint HTML Immediately] F --> H[Download Client JS Bundle] H --> I[Hydrate Client Components Only] G --> J[Fully Interactive Page] I --> J

Next.js App Router: Server Components với Client Components

Trong Next.js App Router (13+), mặc định mọi component đều là Server Component. Đây là bước chuyển lớn so với Pages Router đời cũ, và cũng là phần bị hiểu sai nhiều nhất của Next.js hiện đại.

Vấn đề

Đa số dev quen với Create React App hay Pages Router cứ dán 'use client' lên đầu mọi file "cho chắc ăn". Làm vậy là phá hỏng hết ý nghĩa của Server Component: cuối cùng vẫn gửi từng đó JavaScript xuống browser như cũ, chỉ tốn thêm công thôi.

Server Component (mặc định)

Server Component chỉ render trên server. Không có tí JS nào của nó gửi xuống browser. Nó await data trực tiếp được, đụng database được, xài secret an toàn được, vì code đó không bao giờ rời khỏi server.

// app/products/page.tsx
export default async function ProductsPage() {
    const res = await fetch('https://api.example.com/products', {
        next: { revalidate: 60 },
    })
    const products = await res.json()

    return (
        <ul>
            {products.map((p: { id: string; name: string }) => (
                <li key={p.id}>{p.name}</li>
            ))}
        </ul>
    )
}

Không có useState, không useEffect, không event handler gì hết ở đây - cũng không dính vụ client-side fetch waterfall luôn.

Client Component ('use client')

Client Component cần cho mấy chỗ tương tác: hook, event listener, API chỉ có ở browser (window, localStorage), hay thư viện bên thứ ba đụng tới DOM.

'use client'

import { useState } from 'react'

export default function LikeButton() {
    const [liked, setLiked] = useState(false)

    return (
        <button onClick={() => setLiked(!liked)}>
            {liked ? 'Đã thích' : 'Thích'}
        </button>
    )
}

Directive 'use client' đánh dấu cái ranh giới - mọi thứ import bên dưới nó cũng bị gửi xuống browser luôn, nên giữ component này nhỏ gọn thôi, đẩy nó xuống tận lá của cây component.

Luồng render thật sự chạy ra sao

Mỗi request, luồng chạy như vầy:

graph TD A[Request Từ Browser] --> B[Next.js Server] B --> C[Render Server Component] C --> D[Fetch Data / DB / Secret] D --> E[Tạo HTML + RSC Payload] E --> F[Gửi Response Về Browser] F --> G[Vẽ HTML Ngay Lập Tức] F --> H[Tải Client JS Bundle] H --> I[Chỉ Hydrate Client Component] G --> J[Trang Tương Tác Hoàn Chỉnh] I --> J

Hai chuyện xảy ra song song ngay khi response về tới: HTML tĩnh vẽ liền (first contentful paint nhanh), và chỉ mấy "đảo" Client Component mới được hydrate với JS. Output của Server Component không bao giờ chạy lại hay hydrate trên browser.

Khi nào xài cái nào

  • Server Component (mặc định): lấy data, đọc database, xài API key/secret, render UI tĩnh hoặc gần như tĩnh, giảm bundle size.
  • Client Component ('use client'): useState/useReducer, useEffect, event handler (onClick, onChange), API của browser, context provider, thư viện UI bên thứ ba đụng DOM.

Mấy Cái Bẫy Hay Gặp

  • Dán 'use client' lên cả trang chỉ vì có một nút bấm tương tác - đẩy 'use client' xuống đúng cái nút đó thôi, đừng dán cả trang.
  • Import Server Component vào trong Client Component không hoạt động như mình nghĩ đâu - truyền Server Component vào dạng children hoặc prop thay vì import trực tiếp.
  • Fetch secret bên trong Client Component là rò rỉ nó ra bundle browser liền. Giữ API key với DB call chỉ trong Server Component thôi.
  • Quên next: { revalidate } trên mấy lệnh fetch, làm route âm thầm bị chuyển qua static rendering hoàn toàn trong khi mình lại muốn data mới.

Mặc định xài Server Component cho mọi thứ. Chỉ thêm 'use client' ở component lá nhỏ nhất thật sự cần tương tác thôi - bundle size của mình (và người dùng của mình) sẽ cảm ơn mình đó.

Cùng chủ đề Next.js