Next.js 客户端组件(Client Components)与服务端组件(Server Components)详解
本文基于 Next.js 官方文档Server and Client Components整理,覆盖使用场景、渲染原理、组合规则、最佳实践。
一、核心定位
- 默认类型:App Router 中
layout.tsx/page.tsx默认为服务端组件(Server Components)。 - 分工原则:按运行环境拆分逻辑——服务端管渲染与数据,客户端管交互与浏览器 API。
二、使用场景(官方明确边界)
1. 客户端组件 Client Components(必须加'use client')
满足以下任一需求时使用:
- 需要组件状态:
useState/useReducer/useRef - 需要事件处理:
onClick/onChange等 - 需要生命周期:
useEffect - 需要浏览器专属 API:
window/localStorage/navigator - 需要自定义 Hooks、React Context
- 第三方交互组件(轮播、图表、富文本)
2. 服务端组件 Server Components(默认,无需标记)
满足以下任一需求时使用:
- 就近获取数据:直连数据库、接口,无额外 API 层
- 安全使用密钥:API Key、Token 不暴露到客户端
- 减少客户端 JS 体积:服务端渲染,不发 JS 到浏览器
- 提升首屏性能:优化 FCP,支持流式渲染
- 纯展示、无交互的静态内容(文章、标题、列表)
三、渲染运行原理(官方流程)
1. 服务端渲染阶段
- 服务端组件渲染为RSC Payload(React 服务端组件二进制结构)
- 客户端组件代码 + RSC 用于预渲染HTML
- 按路由分段(layout / page)拆分,支持流式传输
2. 客户端首次加载
- 先展示非交互 HTML,快速呈现内容
- 用 RSC 对齐服务端/客户端组件树
- 对客户端组件执行水合(hydration),绑定事件变可交互
3. 后续导航
- 预加载 & 缓存 RSC,实现秒切路由
- 客户端组件完全在浏览器渲染,不再服务端生成 HTML
关键概念:RSC Payload
- 服务端组件渲染结果
- 客户端组件占位与 JS 引用
- 服务端 → 客户端的可序列化 props
四、基础写法规范
1. 声明方式
- 服务端组件:无指令,默认就是
- 客户端组件:文件顶部第一行加
'use client'(必须在所有 import 之前)
// 客户端组件示例 'use client' import { useState } from 'react' export default function Counter() { const [count, setCount] = useState(0) return <button onClick={() => setCount(count+1)}>{count}</button> }2. 数据传递
- 服务端 → 客户端:用Props传递(必须可序列化)
- 客户端 → 服务端:不直接传组件,只能传可序列化数据
五、组件组合模式(官方推荐)
1. 服务端嵌套客户端(最常用)
- 页面/布局(服务端)→ 嵌入交互按钮/表单(客户端)
- 服务端取数,通过 props 传给客户端做交互
// page.tsx(服务端) import LikeButton from './LikeButton' export default async function Page({ params }) { const post = await getPost(params.id) return <LikeButton likes={post.likes} /> }2. 客户端嵌套服务端
- 客户端组件用
children作为插槽 - 服务端组件作为子元素传入,仍在服务端渲染
- 示例:弹窗(客户端)包裹购物车(服务端)
// Modal(客户端) 'use client' export default function Modal({ children }) { return <div>{children}</div> } // Page(服务端) import Modal from './Modal' import Cart from './Cart' // 服务端组件 export default function Page() { return <Modal><Cart /></Modal> }3. Context 提供者
- Context 只能在客户端组件创建
- 在根布局(服务端)引入包裹全局,实现全局状态共享
// ThemeProvider(客户端) 'use client' import { createContext } from 'react' export const ThemeContext = createContext() export default function ThemeProvider({ children }) { return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider> } // 根布局(服务端) import ThemeProvider from './ThemeProvider' export default function RootLayout({ children }) { return <html><body><ThemeProvider>{children}</ThemeProvider></body></html> }4. 第三方组件兼容
- 依赖客户端特性的库,需包裹一层
'use client'再使用 - 库作者应在入口加
'use client'方便用户直接使用
六、性能与安全最佳实践
最小化客户端边界
只给真正需要交互的组件加'use client',避免整页变成客户端组件,减小 bundle 体积。环境隔离防污染
- 服务端代码:用
server-only包防止被引入客户端 - 客户端代码:可用
client-only标记浏览器专属逻辑 - 环境变量:非
NEXT_PUBLIC_前缀不会注入客户端,防密钥泄露
- 服务端代码:用
Provider 尽量下沉
不要包裹整个html,缩小客户端渲染范围,优化服务端静态内容缓存
七、快速判断口诀(官方思路)
- 要交互、状态、浏览器 API→ 客户端组件(
'use client') - 要取数、安全、减包、提速→ 服务端组件(默认)
- 服务端管数据与渲染,客户端管交互,组合使用最稳
八、核心对比表
| 维度 | 服务端组件 | 客户端组件 |
|---|---|---|
| 声明 | 默认,无指令 | 必须'use client' |
| 渲染位置 | 服务端 | 客户端 |
| 交互能力 | 无 | 完整(state/effect/事件) |
| 数据获取 | 直连数据库/API | 只能走接口 |
| 安全密钥 | 可安全使用 | 不可暴露 |
| 客户端 JS | 零打包 | 需发送 JS |
| 首屏性能 | 优 | 一般 |
| 适用场景 | 展示、取数、SEO | 交互、状态、浏览器 API |
