创见博客
Server Component 和 Client Component 是什么?有什么区别?
七崽爱吃小饼干2026/07/16阅读 3专栏 Next.js/React

在 Next.js App Router 中,经常会遇到两个概念:

  • Server Component
  • Client Component

很多人第一次接触时会有一个疑问:

既然 Client Component 也可以在服务端生成 HTML,然后到浏览器再水合,那为什么还要区分 Server Component 和 Client Component?为什么不全部都写成 Client Component?

这篇文章就围绕三个问题展开:

  1. 什么是 Server Component?
  2. 什么是 Client Component?
  3. 为什么不能全部用 Client Component?

什么是 Server Component?

Server Component,也就是服务端组件。

它的核心特点是:

组件只在服务端执行,组件代码不会发送到浏览器。

也就是说,Server Component 的职责是:

在服务端把数据和组件逻辑处理好,然后生成可以发送给浏览器的结果。

例如:

tsx
export default async function ArticlePage() {
  const article = await getArticle();

  return (
    <article>
      <h1>{article.title}</h1>
      <div>{article.content}</div>
    </article>
  );
}

这个组件可以直接在服务端读取数据:

tsx
const article = await getArticle();

这里的 getArticle 可以访问数据库、调用内部服务、读取环境变量,甚至使用服务端密钥。因为这些代码不会被打包到浏览器里。

Server Component 适合用来渲染:

  • 文章正文
  • 商品详情
  • 文档内容
  • 静态布局
  • 服务端数据列表
  • SEO 友好的页面主体

只要一个组件不需要浏览器交互,通常都可以优先考虑做成 Server Component。

什么是 Client Component?

Client Component,也就是客户端组件。

在 Next.js 中,只要文件顶部写了:

tsx
"use client";

这个文件导出的组件就是 Client Component。

例如:

tsx
"use client";

import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      count: {count}
    </button>
  );
}

Client Component 的核心特点是:

组件代码会被打包发送到浏览器,并且可以在浏览器中完成 hydration 和交互。

它可以使用:

  • useState
  • useEffect
  • useReducer
  • onClick
  • onChange
  • window
  • document
  • localStorage
  • navigator

这些能力都依赖浏览器运行时,所以必须放在 Client Component 中。

Client Component 适合用来处理:

  • 按钮点击
  • 表单输入
  • 弹窗
  • 下拉菜单
  • 复制代码
  • 点赞收藏
  • 评论输入框
  • 拖拽排序
  • 页面滚动监听
  • 本地缓存读取

简单说,凡是依赖用户交互或浏览器 API 的部分,都应该放到 Client Component 中。

Client Component 也会生成 HTML 吗?

会。

这是很多人容易误解的地方。

Client Component 不等于“只在浏览器里渲染”。

在 Next.js App Router 中,Client Component 通常也会参与服务端预渲染。服务端会先生成它的初始 HTML,然后浏览器拿到 HTML 后再加载对应的 JS,最后进行 hydration。

例如这个组件:

tsx
"use client";

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      count: {count}
    </button>
  );
}

服务端返回的 HTML 里可能已经有:

html
<button>count: 0</button>

用户可以立刻看到按钮。

但在 hydration 完成之前,这个按钮还没有 React 事件绑定,点击不会触发 setCount。

等浏览器加载 JS 并完成 hydration 后:

tsx
onClick={() => setCount(count + 1)}

才会真正生效。

所以 Client Component 的准确理解不是“只能在客户端渲染”,而是:

Client Component 会发送 JS 到浏览器,并且可以在浏览器中恢复状态和绑定交互。

Client Component 服务端预渲染,不等于 CSR

前面提到,Client Component 通常也可以在服务端生成初始 HTML,然后到浏览器再 hydration。

但这里需要区分两个概念:

  1. 组件是不是 Client Component。
  2. 核心数据是在服务端获取,还是在浏览器运行时获取。

Client Component 可以服务端预渲染 HTML,但如果核心数据是在浏览器运行时通过 useEffect + fetch 请求回来再渲染,那这个页面的核心内容就会退化成 CSR。

例如文章页如果这样写:

tsx
"use client";

import { useEffect, useState } from "react";

export default function ArticlePage() {
  const [article, setArticle] = useState(null);

  useEffect(() => {
    fetch("/api/articles/1")
      .then(res => res.json())
      .then(data => setArticle(data));
  }, []);

  if (!article) return <div>loading...</div>;

  return (
    <article>
      <h1>{article.title}</h1>
      <div>{article.content}</div>
    </article>
  );
}

这种情况下,服务端返回的初始 HTML 里很可能只有:

html
<div>loading...</div>

文章标题和正文并不在初始 HTML 中。

浏览器需要经历:

  1. 下载 JavaScript。
  2. React hydration。
  3. 执行 useEffect。
  4. 请求 /api/articles/1。
  5. 接口返回文章数据。
  6. setState 触发重新渲染。

文章正文才会真正出现在页面上。

这就是典型的 CSR 数据渲染路径。

对于文章页、商品详情页、文档页这类 SEO 核心页面来说,这种方式并不理想。因为搜索引擎和用户一开始拿到的 HTML 里没有核心内容,首屏展示也会更慢。

更好的方式是把核心数据获取放在服务端:

tsx
export default async function ArticlePage() {
  const article = await getArticle();

  return (
    <article>
      <h1>{article.title}</h1>
      <div>{article.content}</div>
    </article>
  );
}

这样流程会变成:

txt
服务端获取文章数据
服务端生成包含正文的 HTML
浏览器直接展示文章内容
需要交互的局部组件再 hydration

所以,判断一个页面是不是退化成 CSR,关键不只是看它用了 Server Component 还是 Client Component,而是看核心内容的数据获取发生在哪里。

如果文章正文、商品信息、文档内容这些核心数据在服务端获取,并参与初始 HTML 生成,那么它仍然具备 SSR、SSG 或 ISR 的优势。

如果这些核心数据要等浏览器运行时请求回来后才渲染,那么核心内容就退化成了 CSR。

这也是为什么文章详情页通常应该让正文服务端直出,而把点赞、收藏、复制代码、评论输入框这些交互能力拆成局部 Client Component。

Server Component 和 Client Component 的区别

两者最大的区别不是“有没有 HTML”,而是:

组件代码是否会发送到浏览器,以及是否需要 hydration。

可以这样对比:

对比项Server ComponentClient Component
是否在服务端执行是是,通常会预渲染初始 HTML
是否发送组件代码到浏览器否是
是否需要 hydration否是
是否能使用 useState否是
是否能使用 useEffect否是
是否能绑定 onClick否是
是否能访问 window/document否是
是否能直接访问数据库是否
是否能使用服务端密钥是否
适合场景内容、数据、静态 UI交互、状态、浏览器 API

为什么不能全部用 Client Component?

既然 Client Component 也能生成 HTML,也能交互,那为什么不全部用 Client Component?

原因是:这样会退回到传统 SSR 的模式,成本更高。

传统 SSR 的思路是:

txt
服务端生成 HTML
客户端下载整页 JS
客户端整页 hydration

它的问题是:页面上所有组件的代码都要发给浏览器,所有组件都要参与 hydration。

但很多组件其实只是静态展示。

例如文章页中:

  • 标题
  • 作者名
  • 发布时间
  • 正文段落
  • 列表
  • 表格
  • 静态图片
  • SEO 信息

这些内容不需要在浏览器里执行组件逻辑,也不需要绑定事件。

如果全部写成 Client Component,这些静态内容的组件代码也会被打包进客户端 JS,浏览器还要参与 hydration。

这会带来几个问题。

一、客户端 JavaScript 变大

Client Component 的代码需要发送到浏览器。

如果整篇文章、Markdown 渲染器、语法解析器、布局组件、数据处理逻辑都写成 Client Component,那么这些代码都会进入客户端 bundle。

用户打开页面时,不仅要下载 HTML,还要下载更多 JS。

JS 越大,浏览器解析和执行成本越高,首屏越容易变慢。

对于内容站、文档站、博客这种页面来说,正文通常很多,但大部分内容不需要交互。把这些内容都变成客户端组件,是不必要的浪费。

Server Component 可以避免这个问题。

它只在服务端执行,浏览器拿到的是结果,不需要下载组件实现。

二、hydration 成本变高

Client Component 不只是下载 JS,还需要 hydration。

hydration 的过程大致包括:

  1. 加载组件代码。
  2. 在浏览器中重新构建 React 组件树。
  3. 和服务端生成的 HTML 对齐。
  4. 初始化状态。
  5. 绑定事件。

如果页面很大,hydration 成本也会很高。

例如一篇长文章中有大量标题、段落、列表、代码块和表格,如果整篇文章都作为 Client Component 渲染,浏览器就要为这些内容做更多 hydration 工作。

但实际上,很多内容只是展示,不需要交互。

Server Component 的优势是:

不需要 hydration。

它在服务端已经完成渲染,浏览器只需要展示 HTML。

三、容易暴露服务端逻辑

Server Component 可以直接使用服务端能力:

tsx
const article = await db.article.findUnique({
  where: { id },
});

也可以安全读取服务端环境变量:

tsx
const secret = process.env.SECRET_KEY;

因为这些代码不会进入浏览器。

但 Client Component 会被打包发送到浏览器,所以不能直接访问数据库,也不能包含服务端密钥或内部逻辑。

如果全部写成 Client Component,就必须把数据获取改成接口请求:

tsx
useEffect(() => {
  fetch("/api/article/1")
    .then(res => res.json())
    .then(data => setArticle(data));
}, []);

这样不仅多了一次客户端请求,也让页面初始内容更依赖浏览器执行 JS。

四、数据获取链路更绕

Server Component 可以直接在组件里获取数据:

tsx
export default async function ArticlePage() {
  const article = await getArticle();

  return <Article article={article} />;
}

这个流程是:

txt
服务端获取数据
服务端生成 HTML
浏览器直接展示内容

如果全部用 Client Component,常见流程会变成:

txt
服务端返回页面框架
浏览器下载 JS
浏览器执行 JS
客户端请求接口
接口返回数据
客户端 setState
页面渲染正文

这会让首屏内容出现得更晚,也不利于 SEO。

对于文章、商品、文档这类页面,核心内容越早出现在 HTML 中越好。

五、缓存能力更弱

Next.js 的 Server Component 可以很好地结合服务端缓存能力,例如:

  • 静态生成
  • ISR
  • revalidatePath
  • 服务端 fetch 缓存
  • 路由级缓存

比如文章详情页可以使用 ISR:

txt
第一次请求生成 HTML
后续请求复用缓存
文章更新后按需 revalidate

这非常适合内容站。

如果全部改成 Client Component,很多数据就要在浏览器运行时请求,服务端页面缓存的价值会下降。

正确的组件拆分方式

比较好的做法不是“全部 Server Component”,也不是“全部 Client Component”。

而是:

能静态渲染的部分用 Server Component。 必须交互的最小部分用 Client Component。

例如一个文章阅读页可以这样拆:

txt
ArticlePage             Server Component
ArticleHeader           Server Component
ArticleContent          Server Component
MarkdownRenderer        Server Component
CodeBlockCopyButton     Client Component
LikeButton              Client Component
CollectButton           Client Component
CommentEditor           Client Component
OutlineScrollSpy        Client Component

这样做的好处是:

  • 文章正文可以服务端直出。
  • SEO 友好。
  • 浏览器不用下载整篇 Markdown 渲染逻辑。
  • 只有复制、点赞、收藏、评论这些交互部分需要 JS。
  • 页面整体更轻。

一个代码块的例子

比如文章中的代码块。

代码内容本身是静态的,应该出现在服务端 HTML 中。

但“复制代码”按钮需要调用浏览器的 Clipboard API:

tsx
navigator.clipboard.writeText(code);

这就必须放在 Client Component 中。

所以可以这样设计:

txt
MarkdownRenderer:Server Component
CodeBlock:Client Component

服务端先输出代码块 HTML:

html
<pre>
  <code>console.log("hello");</code>
</pre>
<button>复制代码</button>

浏览器先看到代码和按钮。

hydration 完成后,按钮才具备复制能力。

这就是 Server Component 和 Client Component 配合的典型场景:

内容由服务端保证首屏和 SEO,交互由客户端增强。

如何判断一个组件该放哪边?

可以用几个问题判断:

  1. 这个组件需要 useState 吗?
  2. 这个组件需要 useEffect 吗?
  3. 这个组件需要绑定点击、输入、提交事件吗?
  4. 这个组件需要访问 window/document/localStorage/navigator 吗?
  5. 这个组件依赖浏览器-only 的第三方库吗?

如果答案是“是”,它通常应该是 Client Component。

再问几个问题:

  1. 这个组件只是展示数据吗?
  2. 这个组件可以直接在服务端拿到数据吗?
  3. 这个组件不需要用户交互吗?
  4. 这个组件是否希望减少客户端 JS?
  5. 这个组件是否涉及数据库、文件系统或服务端密钥?

如果答案是“是”,它更适合 Server Component。

总结

Server Component 和 Client Component 的区别,不是有没有 HTML,而是:

  • Server Component 只在服务端执行,不发送组件代码到浏览器,不需要 hydration。
  • Client Component 可以服务端预渲染初始 HTML,但组件代码会发送到浏览器,并在浏览器中 hydration。

不能全部用 Client Component,是因为这样会导致:

  • 客户端 JS 变大。
  • hydration 成本变高。
  • 数据获取链路更绕。
  • 服务端逻辑更难安全复用。
  • 缓存和 SEO 效果变差。

更好的实践是:

静态内容、服务端数据和 SEO 主体用 Server Component。 用户交互、浏览器 API 和局部状态用 Client Component。

这样既能保留服务端渲染的性能和 SEO 优势,又能在需要的地方提供完整的客户端交互体验。

评论
0/100