Next.jsReactServer ComponentsApp Router

Next.js Server 与 Client Components 怎么选

解释 App Router 中 Server Component 与 Client Component 的执行位置、数据获取、序列化边界和 hydration,并给出缩小客户端 bundle 的组件拆分方法。

·更新于 ·阅读约 10 分钟·计算中...
Next.js Server 与 Client Components 怎么选

Next.js App Router 中,页面和布局默认是 Server Components。只有组件需要 state、事件处理器、Effect、Context 消费或浏览器 API 时,才在文件顶部添加 "use client"。最佳实践不是“全部服务端”,而是把客户端边界压缩到真正需要交互的区域。

目录

选择表

需求Server ComponentClient Component
直接读取数据库或服务端密钥✅❌
服务端获取公开内容✅可通过 API,但通常没必要
useState、useReducer❌✅
onClick、onChange❌✅
useEffect、浏览器 API❌✅
减少发送到浏览器的 JavaScript✅❌

默认的 Server Component

代码
// app/blog/page.tsx
export default async function BlogPage() {
  const posts = await getPosts()

  return (
    <main>
      <h1>文章</h1>
      <PostList posts={posts} />
    </main>
  )
}

Server Component 可以在服务端直接读取数据,不会把该组件的实现代码作为交互 JavaScript 发到浏览器。但它不能使用事件处理器、state、Effect 或 window。

最小化 use client 边界

搜索框需要交互时,只标记搜索框:

代码
// components/SearchBox.tsx
"use client"

import { useState } from "react"

export function SearchBox() {
  const [query, setQuery] = useState("")
  return <input value={query} onChange={(event) => setQuery(event.target.value)} />
}
代码
// app/blog/page.tsx:仍然是 Server Component
import { SearchBox } from "@/components/SearchBox"

export default async function BlogPage() {
  const posts = await getPosts()
  return (
    <>
      <SearchBox />
      <PostList posts={posts} />
    </>
  )
}

"use client" 声明的是模块图边界。该文件导入的组件和依赖会进入客户端模块图,所以不要在大型页面或根 layout 上随意添加。

Server 可以把 UI 传给 Client

Client Component 可以通过 children 接收已经在服务端渲染的内容:

代码
// components/Modal.tsx
"use client"

export function Modal({ children }: { children: React.ReactNode }) {
  return <dialog>{children}</dialog>
}
代码
<Modal>
  <ServerRenderedArticle />
</Modal>

不要在 Client Component 文件里直接导入只能在服务端运行的模块;通过父级组合传入更清楚。

Props 必须可序列化

从 Server Component 传给 Client Component 的 props 会跨越网络边界,应使用可序列化数据。数据库连接、类实例和任意函数不能直接传递;事件回调应在客户端定义,服务器操作使用框架支持的 Server Function/Action 机制。

日期等数据最好转为明确字符串,并在显示时处理时区,避免服务器和浏览器首次渲染结果不同。

首次加载发生什么

Next.js 会在服务端生成 Server Component Payload,并结合 Client Components 预渲染 HTML。浏览器先展示 HTML,再使用 RSC Payload 协调组件树,并通过 JavaScript hydration 让 Client Components 可交互。

因此 Client Component 也可能参与首屏服务端预渲染;“Client Component 只在浏览器渲染”是不准确的。若服务端与客户端首次输出不同,就可能出现 hydration mismatch,排查方法见 React Hydration Failed 指南。

常见错误

  • 为了使用一个按钮,在整个页面顶部添加 "use client"。
  • 在 Server Component 中使用 window 或事件处理器。
  • 把私密环境变量传给 Client Component。
  • 跨边界传递不可序列化对象。
  • 使用客户端 Effect 重新请求服务端本来就能获取的首屏数据。
  • 用 typeof window 直接改变首次渲染 JSX,导致 hydration 不一致。

推荐拆分方式

代码
Server Page
├── Server Header
├── Server ArticleList
│   └── Client FavoriteButton
└── Client SearchPanel
    └── Server 传入的初始数据

先保持页面为 Server Component,再把交互叶子节点下沉为 Client Component。只有多个交互组件确实共享状态时,才把客户端边界向上移动。

参考资料

订阅 FreeMac

每周精选:免费 Mac 软件评测、可信来源更新、替代方案和少折腾指南。