Next.js 中 SSR 与 RSC 的区别:别再傻傻分不清了!

49 阅读6分钟

一、🤔 先来个“厨房比喻”回顾

还记得我们之前把前端开发比作餐厅吗?今天换个角度,把网站比作一道菜:

阶段传统做法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 数据库/APIRSC可以直接查数据库,更直观
📈 JS 包体积组件JS仍会发到客户端RSC代码永不发到客户端RSC大幅减少包体积,早期测试显示减少18-29%
🚰 流式渲染传统SSR是“全有或全无”天然支持流式 + SuspenseRSC可以分块发送,优先显示关键内容

关键点解读

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 体积?

看个实际例子 :

// ❌ 传统 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}} />;
}
// ✅ 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 时代)

// 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 时代)

// 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>
  );
}
// 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 的完整渲染流程

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

场景原因
按钮、表单、输入框需要 onClickonChange 等事件
轮播图、选项卡需要 useState 管理 UI 状态
实时数据更新需要 useEffect 或 WebSocket
使用浏览器 API需要访问 windowlocalStorage

5.3 黄金法则

尽量用 RSC,只在需要交互的地方用 Client Component

这个原则能让你的应用:

  • 🚀 首屏加载飞快(HTML 直接展示)
  • 📦 JS 包体积最小(交互部分才发代码)
  • 🔍 SEO 最佳(内容完整)
  • 💰 服务器资源最省(计算放服务器,客户端只管展示)

六、🎯 总结:一句话记住区别

概念一句话解释
SSR(服务端渲染)把 React 组件变成 HTML 发到浏览器,让首屏快、SEO 好
RSC(服务端组件)让组件永远留在服务器执行,0 JS 发到客户端,大幅减小包体积

两者的关系:RSC 依赖 SSR 来生成初始 HTML,但通过把大部分组件留在服务器,让客户端只加载真正需要的交互代码 。

给初学者的实践建议

  1. 用 Next.js App Router(默认全是 RSC)
  2. 先从纯展示页面开始:博客、产品列表、文档站,直接用 RSC
  3. 遇到交互再切 Client:需要按钮、输入框时,才加 'use client'
  4. 保持“客户端隔离”:尽量把交互部分拆成小组件,让大部分页面保持 RSC

最后送大家一句话:SSR 解决了“看得到”,RSC 解决了“带得少”。两者结合,才是现代 React 应用的终极形态!💪