Trung bình JavaScript

Next.js App Router: Server Components vs Client Components

Server Components run only on the server; Client Components hydrate in the browser. See how Next.js App Router renders both, and when to actually reach for 'use client'.

14 Th07, 2026 3 phút 8 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 vs Client Components

In the Next.js App Router (13+), every component is a Server Component by default. This is a big shift from the old Pages Router, and it's the single most misunderstood part of modern Next.js.

The Problem

Most developers coming from Create React App or the Pages Router slap 'use client' at the top of every file "just in case." That defeats the purpose of Server Components: you end up shipping the same amount of JavaScript to the browser as before, just with extra steps.

Server Components (the default)

Server Components render only on the server. No JS for them is sent to the browser. They can await data directly, touch a database, or use secrets safely, because that code never leaves the 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>
  )
}

No useState, no useEffect, no event handlers here — and no client-side fetch waterfall either.

Client Components ('use client')

Client Components are needed for interactivity: hooks, event listeners, browser-only APIs (window, localStorage), or third-party libraries that rely on the DOM.

'use client'

import { useState } from 'react'

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

  return (
    <button onClick={() => setLiked(!liked)}>
      {liked ? 'Liked' : 'Like'}
    </button>
  )
}

The 'use client' directive marks the boundary — everything imported below it also ships to the browser, so keep these components small and push them to the leaves of the tree.

How Rendering Actually Flows

On every request, the flow looks like this:

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

Two things happen in parallel once the response lands: the static HTML paints right away (fast first contentful paint), and only the Client Component "islands" get hydrated with JS. The Server Component output is never re-executed or hydrated in the browser.

When to Use Which

  • Server Component (default): fetching data, reading from a database, using API keys/secrets, rendering static or mostly-static UI, reducing bundle size.
  • Client Component ('use client'): useState/useReducer, useEffect, event handlers (onClick, onChange), browser APIs, context providers, third-party UI libraries that touch the DOM.

Common Pitfalls

  • Marking a whole page 'use client' because of one interactive button — push 'use client' down to that button only, not the entire page.
  • Importing a Server Component into a Client Component doesn't work the way you'd expect — pass the Server Component in as children or a prop instead of importing it directly.
  • Fetching secrets inside a Client Component leaks them into the browser bundle. Keep API keys and DB calls in Server Components only.
  • Forgetting next: { revalidate } on fetch calls, which silently opts routes into fully static rendering when you actually wanted fresh data.

Default to Server Components everywhere. Only add 'use client' at the smallest leaf component that truly needs interactivity — your bundle size (and your users) will thank you.

Cùng chủ đề JavaScript