一、🤔 先来个"厨房比喻"回顾
还记得我们之前把前端开发比作餐厅吗?今天换个角度,把网站比作一道菜:
| 阶段 | 传统做法 | SSR做法 | RSC做法 |
|---|---|---|---|
| 备菜 | 把所有食材和菜谱(JS代码)端上桌 | 后厨炒好整盘菜端上桌 | 后厨把菜分成"预制菜"和"现炒菜" |
| 上菜 | 客人自己炒(客户端执行JS) | 直接吃(显示HTML) | 先吃预制菜,再吃现炒菜 |
| 交互 | 炒菜时就能尝味道 | 吃完后才能点调料 | 预制菜不用动,现炒菜可以随时加料 |
这个比喻可能有点绕,别急,下面我们逐条拆解!
二、📊 SSR vs RSC:一张表看懂核心区别
| 对比维度 | SSR(服务端渲染) | RSC(React服务端组件) | 关键差异解读 |
|---|---|---|---|
| 🎯 执行环境 | 每次请求时在服务器执行 | 在服务器执行,但结果可缓存 | SSR每个请求都重新跑一遍,RSC可以复用 |
| 📦 输出产物 | 完整的 HTML 文档 | 特殊的 RSC Payload(组件树描述格式) | RSC不直接输出HTML,而是输出一种中间格式 |
| 🔄 交互能力 | 需要 hydrate 激活 | 零 JS 包体积,完全无交互 | RSC组件不能用 useState/useEffect |
| 💾 数据获取 | 通过框架API(如 getServerSideProps) |
直接在组件里 await 数据库/API |
RSC可以直接查数据库,更直观 |
| 📈 JS 包体积 | 组件JS仍会发到客户端 | RSC代码永不发到客户端 | RSC大幅减少包体积,早期测试显示减少18-29% |
| 🚰 流式渲染 | 传统SSR是"全有或全无" | 天然支持流式 + Suspense | RSC可以分块发送,优先显示关键内容 |
关键点解读
1. 关于"use client"的真相
很多初学者以为加了 'use client' 就是客户端渲染(CSR),这是误解!
在 Next.js 的 App Router 中,所有组件默认都是 RSC ,它们先在服务器上执行生成 HTML。即使你加上
'use client',这个组件依然会经历 SSR 过程,只是它的 JS 代码会被打包发送到客户端,用于后续的交互和水合(hydration)。
简单说:SSR 是"必选项",RSC 是"可选项"------SSR永远发生,RSC决定是否把JS发下去。
2. 为什么 RSC 能减少 JS 体积?
看个实际例子 :
javascript
// ❌ 传统 React 组件(所有代码都发到客户端)
import marked from "marked"; // 35.9K
import sanitizeHtml from "sanitize-html"; // 206K
function NoteWithMarkdown({text}) {
const html = sanitizeHtml(marked(text));
return <div dangerouslySetInnerHTML={{__html: html}} />;
}
javascript
// ✅ RSC 版本(服务端组件,依赖零体积)
import marked from "marked"; // 0K(只在服务器)
import sanitizeHtml from "sanitize-html"; // 0K
function NoteWithMarkdown({text}) {
const html = sanitizeHtml(marked(text));
return <div dangerouslySetInnerHTML={{__html: html}} />;
}
看到了吗?同样的代码,放在 RSC 里,这些库的代码就永远不会发给客户端! 这就是 RSC 最核心的威力 。
三、🎯 实战代码:一眼看出区别
3.1 传统 SSR(Pages Router 时代)
jsx
// pages/products/[id].jsx (Pages Router)
export async function getServerSideProps({ params }) {
// 1. 在服务器获取数据
const res = await fetch(`https://api.example.com/products/${params.id}`);
const product = await res.json();
// 2. 返回 props
return { props: { product } };
}
export default function ProductPage({ product }) {
// 3. 这个组件代码会发送到客户端
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<button onClick={() => addToCart(product.id)}>加入购物车</button>
</div>
);
}
问题在哪? 整个组件的 JS(包括 addToCart 逻辑和渲染代码)都会被打包发给客户端,即使页面主要展示静态内容 。
3.2 RSC + Client Component(App Router 时代)
jsx
// app/products/[id]/page.jsx (默认是 RSC)
import { db } from '@/lib/db'; // 直接连数据库!
import AddToCartButton from './AddToCartButton'; // 客户端组件
export default async function ProductPage({ params }) {
// ✅ RSC:直接查数据库,代码永远留在服务器
const product = await db.product.findUnique({
where: { id: params.id }
});
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
{/* 只有这个按钮的JS会发到客户端 */}
<AddToCartButton productId={product.id} />
</div>
);
}
jsx
// app/products/[id]/AddToCartButton.jsx
'use client'; // 标记为客户端组件
import { useState } from 'react';
export default function AddToCartButton({ productId }) {
const [isAdding, setIsAdding] = useState(false);
const handleClick = async () => {
setIsAdding(true);
await fetch('/api/cart/add', {
method: 'POST',
body: JSON.stringify({ productId })
});
setIsAdding(false);
};
return (
<button onClick={handleClick} disabled={isAdding}>
{isAdding ? '添加中...' : '加入购物车'}
</button>
);
}
RSC 版本的优势:
- 产品信息渲染的代码 0 KB 发到客户端
- 只有
AddToCartButton的 JS(大约 1KB)会打包发送 - 数据获取直接在服务器完成,没有 API 层,没有
useEffect瀑布流
四、🧩 它们是怎么配合工作的?
很多人以为 RSC 会取代 SSR,大错特错! 它们其实是"搭档"关系 :
Next.js App Router 的完整渲染流程
css
1️⃣ 用户请求页面
↓
2️⃣ 服务器执行所有组件(无论是 RSC 还是 Client Component)
↓
3️⃣ 生成 HTML 流式返回(SSR 干的活,让首屏快)
↓
4️⃣ 同时生成 RSC Payload(描述组件树的结构)
↓
5️⃣ 浏览器接收 HTML,立即显示内容
↓
6️⃣ 加载客户端组件所需的 JS
↓
7️⃣ 水合(hydrate)客户端组件,使其可交互
↓
8️⃣ 后续导航时,直接用 RSC Payload 更新,无需刷新页面
关键理解:
- SSR 负责"首次展示":把页面变成 HTML 让用户第一时间看到
- RSC 负责"后续效率":减少 JS 体积,让更新更轻量
用个比喻:SSR 是餐厅的"成品菜"(端上来就能吃),RSC 是"预制菜"(稍微加热就能吃),Client Component 是"现场烹饪"(需要等但更新鲜)。
五、✅ 什么时候用哪个?
5.1 优先用 RSC(服务端组件)
| 场景 | 原因 |
|---|---|
| 博客文章、新闻详情 | 内容静态,无需交互,0 JS 发送 |
| 商品列表、产品页 | 需要 SEO,数据在服务器获取 |
| 用户资料展示 | 读取数据库,无需客户端状态 |
| Markdown 渲染 | 可以把 marked、sanitize-html 留在服务端 |
5.2 必须用 Client Component
| 场景 | 原因 |
|---|---|
| 按钮、表单、输入框 | 需要 onClick、onChange 等事件 |
| 轮播图、选项卡 | 需要 useState 管理 UI 状态 |
| 实时数据更新 | 需要 useEffect 或 WebSocket |
| 使用浏览器 API | 需要访问 window、localStorage 等 |
5.3 黄金法则
尽量用 RSC,只在需要交互的地方用 Client Component
这个原则能让你的应用:
- 🚀 首屏加载飞快(HTML 直接展示)
- 📦 JS 包体积最小(交互部分才发代码)
- 🔍 SEO 最佳(内容完整)
- 💰 服务器资源最省(计算放服务器,客户端只管展示)
六、🎯 总结:一句话记住区别
| 概念 | 一句话解释 |
|---|---|
| SSR(服务端渲染) | 把 React 组件变成 HTML 发到浏览器,让首屏快、SEO 好 |
| RSC(服务端组件) | 让组件永远留在服务器执行,0 JS 发到客户端,大幅减小包体积 |
两者的关系:RSC 依赖 SSR 来生成初始 HTML,但通过把大部分组件留在服务器,让客户端只加载真正需要的交互代码 。
给初学者的实践建议
- 用 Next.js App Router(默认全是 RSC)
- 先从纯展示页面开始:博客、产品列表、文档站,直接用 RSC
- 遇到交互再切 Client :需要按钮、输入框时,才加
'use client' - 保持"客户端隔离":尽量把交互部分拆成小组件,让大部分页面保持 RSC
最后送大家一句话:SSR 解决了"看得到",RSC 解决了"带得少"。两者结合,才是现代 React 应用的终极形态!💪