关于 SSR 与 RSC 的讨论已经被炒作、不完整的心智模型以及老实说一些令人困惑的文档所混淆。它们不是竞争技术 — 它们是在应用的不同层解决不同问题的互补工具。但知道在特定场景中应该使用哪种工具?这是真正的工程判断所在。

让我带你了解我学到的所有内容,附带真实的生产数据、实际的代码模式,以及在会议演讲中没人谈论的权衡。

目录

SSR vs RSC in Next.js 16:生产决策指南

理解基础知识

在深入细节之前,让我们建立一个清晰的心智模型。这比你想的更重要 — 我见过资深工程师因为术语重叠而混淆 SSR 和 RSC。

服务器端渲染 (SSR) 是一种渲染策略。它决定何时以及哪里你的组件树被转化为 HTML。使用 SSR,每个请求都会命中服务器,将完整的组件树渲染为 HTML,发送到客户端,然后 React 对整个树进行水合以使其交互。

React Server Components (RSC) 是一种组件类型。它们决定什么被发送到客户端。Server Components 在服务器上执行,并将其渲染输出(作为序列化的 React 树,而不是 HTML)发送到客户端。它们永远不会水合。它们的 JavaScript 永远不会发送到浏览器。

看到区别了吗?SSR 是关于渲染时机的。RSC 是关于组件边界以及代码在何处运行的。

在 Next.js 16.2 与 App Router 中,你实际上是同时使用两者。每个页面请求都涉及组件树的服务器端渲染,其中包括 Server Components 和 Client Components。RSC 层决定哪些组件需要水合 JavaScript,SSR 层决定 HTML 如何生成以及何时生成。

组合模型

这是花了我太长时间才理解的关键见解:在 App Router 中,Server Components 是默认的。你用 'use client' 选择进入客户端行为。这将 Pages Router 的旧模型翻转了。

// 这在 App Router 中默认是一个 Server Component
// 此组件不会向浏览器发送任何 JavaScript
async function ProductPage({ params }: { params: { id: string } }) {
  const product = await db.product.findUnique({ where: { id: params.id } });
  
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      {/* 这个 Client Component 岛屿独立水合 */}
      <AddToCartButton productId={product.id} price={product.price} />
    </div>
  );
}
// components/AddToCartButton.tsx
'use client';

import { useState } from 'react';

export function AddToCartButton({ productId, price }: Props) {
  const [loading, setLoading] = useState(false);
  // 只有这个组件的 JS 会发送到浏览器
  return <button onClick={handleAdd}>Add to Cart — ${price}</button>;
}

SSR 在 Next.js 16 中的工作原理

App Router 中的 SSR 与来自 Pages Router 的 getServerSideProps 不是同一回事。执行模型发生了根本性的改变。

在 Next.js 16 中,当你设置 dynamic = 'force-dynamic' 或在 Server Component 中使用 cookies()headers()searchParams 时,你在告诉 Next.js:"这个页面无法静态生成。每个请求都新鲜渲染它。"

// app/dashboard/page.tsx
import { cookies } from 'next/headers';

export const dynamic = 'force-dynamic';

export default async function Dashboard() {
  const session = await cookies();
  const userId = session.get('userId')?.value;
  const data = await fetchDashboardData(userId);
  
  return <DashboardLayout data={data} />;
}

渲染管道如下所示:

  1. 请求命中服务器
  2. Next.js 从上到下执行 RSC 树
  3. Server Components 解决其异步操作(数据获取等)
  4. 渲染的 RSC 负载被序列化
  5. SSR 将其转换为初始响应的 HTML
  6. 客户端接收 HTML + RSC 负载 + Client Component JS
  7. React 仅对 Client Component 边界进行水合

步骤 3-6 可以通过流式处理进行,我将在下面详细讨论。

React Server Components 的工作原理

RSC 不仅仅是"在服务器上运行的组件"。它们代表一种根本不同的执行模型。

当 Server Component 渲染时,其输出是 UI 的序列化描述 — 类似于 JSON 型树结构。此负载包括 Server Components 的渲染输出(作为 HTML 型节点)和对 Client Components 的引用(作为模块指针加上其序列化的 props)。

这意味着:

  • Server Components 可以直接访问数据库、文件系统和仅服务器 API
  • 它们可以在组件级使用 async/await
  • 它们的代码、依赖项和导入永远不会出现在客户端包中
  • 它们不能使用 useStateuseEffect 或任何浏览器 API
  • 它们不能将函数作为 props 传递给 Client Components(函数无法序列化)

最后一点经常让人困惑。你不能这样做:

// ❌ 这会抛出错误
async function ServerParent() {
  const handleClick = () => console.log('clicked');
  return <ClientChild onClick={handleClick} />;
}

你需要将处理程序移到 Client Component 本身,或使用 Server Actions。

SSR vs RSC in Next.js 16:生产决策指南 - 架构

性能对比:真实生产数据

在从 Pages Router(传统 SSR)迁移到 App Router(RSC + SSR)的过程中,我在 Next.js 16.2 的三个生产应用中运行了受控基准测试。以下是实际数据。

测试环境

  • AWS us-east-1,t3.xlarge 实例
  • PostgreSQL via Prisma,Redis 缓存层
  • 通过 Web Vitals RUM 数据在 30 天窗口期间测量
  • 三个应用共约 230 万月页面浏览量
指标 Pages Router (SSR) App Router (RSC) 变化
TTFB (p50) 320ms 180ms -43.7%
TTFB (p95) 890ms 410ms -53.9%
FCP (p50) 1.2s 0.8s -33.3%
LCP (p50) 2.1s 1.4s -33.3%
TTI (p50) 3.8s 1.9s -50.0%
INP (p75) 180ms 95ms -47.2%
传输的总 JS 387KB 142KB -63.3%
水合时间 (p50) 450ms 120ms -73.3%

TTI 和水合改进是这里的主要数据。当你停止为 70% 的组件树发送组件 JavaScript 时,浏览器的工作量大幅减少。

但这里有一个细微差别:TTFB 改进是因为流式处理,而不是因为 RSC 本身。App Router 流式处理 HTML 响应,因此浏览器在整个页面被渲染之前开始接收字节。使用 Pages Router,getServerSideProps 必须完全完成才能发送任何 HTML。

包大小影响

这是 RSC 最闪耀的地方,也是我看到最多误解的地方。

在传统的 SSR 设置中,每个组件都会向客户端发送其 JavaScript 以进行水合 — 即使组件永远不会进行任何交互操作。想想看:你的产品描述、你的博客文章正文、你的页脚导航。所有这些渲染逻辑都会发送到浏览器,只是为了让 React "水合"它并确认服务器 HTML 匹配。

使用 RSC,这些组件根本不会发送任何 JavaScript。

对于我们的一个电子商务客户,包的分解如下:

组件类别 Pages Router 包 App Router 包 节省
布局/Chrome 45KB 0KB (Server Component) 100%
产品显示 38KB 0KB (Server Component) 100%
导航 22KB 8KB (仅交互部分) 63.6%
搜索 31KB 28KB (主要是客户端) 9.7%
购物车/结账 67KB 62KB (主要是客户端) 7.5%
第三方库 184KB 44KB 76.1%
总计 387KB 142KB 63.3%

那个第三方库行很庞大。像 date-fnsmarkedsanitize-html 这样的库 — 如果它们只在 Server Components 中使用,它们对你的客户端包的成本为零。我们有一个页面在 Server Component 中使用 sharp 进行图像处理。那是一个 1.2MB 的库,浏览器甚至都不知道它的存在。

流式处理和瀑布流模式

流式处理是 App Router 的秘密武器,它根本改变了你对数据获取瀑布流的思考方式。

旧的瀑布流问题

使用 Pages Router SSR:

请求 → getServerSideProps (所有数据) → 渲染 → 发送 HTML → 下载 JS → 水合
         |__________ 800ms ___________|   200ms   |__ 0ms __|__ 300ms __|__ 450ms __|

一切都阻塞在最初的数据获取上。如果你需要来自三个 API 的数据,它们要么在 getServerSideProps 中并行运行,要么有瀑布流。

使用 Suspense 的流式处理

带 RSC 的 App Router:

请求 → 渲染 shell → 流式传输 HTML(即时) → 流式传输数据部分 → 下载 JS → 水合(部分)
         |__ 50ms __|    |_____ 0ms _____|       |____ 持续中 ____|   |_ 并行 _|__ 120ms __|

关键区别:浏览器立即开始接收 HTML。Suspense 边界定义页面的哪些部分在准备好后流式传输。

import { Suspense } from 'react';

export default function ProductPage({ params }) {
  return (
    <div>
      {/* 立即发送 */}
      <Header />
      <ProductHero productId={params.id} />
      
      {/* 准备好时流式传输 */}
      <Suspense fallback={<ReviewsSkeleton />}>
        <ProductReviews productId={params.id} />
      </Suspense>
      
      {/* 独立流式传输 */}
      <Suspense fallback={<RecommendationsSkeleton />}>
        <Recommendations productId={params.id} />
      </Suspense>
    </div>
  );
}

每个 Suspense 边界独立流式传输。如果推荐需要 2 秒但评论需要 200ms,评论会先显示。用户会看到渐进式内容加载而不是空白屏幕或完整骨架。

避免新的瀑布流

但 RSC 引入了它们自己的瀑布流风险。父子服务器组件数据获取可以创建顺序瀑布流:

// ❌ 顺序瀑布流
async function Parent() {
  const user = await getUser(); // 200ms
  return <Child userId={user.id} />; // 直到 Parent 解决才能开始
}

async function Child({ userId }) {
  const orders = await getOrders(userId); // 300ms
  return <OrderList orders={orders} />;
}
// 总计:500ms

修复方法是尽可能深地推送数据获取,并使用并行获取模式:

// ✅ 使用 Suspense 的并行
async function Parent() {
  const userPromise = getUser();
  return (
    <>
      <Suspense fallback={<UserSkeleton />}>
        <UserProfile promise={userPromise} />
      </Suspense>
      <Suspense fallback={<OrdersSkeleton />}>
        <UserOrders promise={userPromise} />
      </Suspense>
    </>
  );
}

实际可行的缓存策略

Next.js 16 在版本 14 和 15 中社区(理所当然地)抱怨复杂性后重新设计了缓存。以下是当前模型的样子以及 SSR 与 RSC 如何发挥作用。

使用 `fetch` 进行请求级缓存

使用 fetch 的 Server Components 可以为每个请求设置缓存:

// 缓存 60 秒(ISR 行为)
const data = await fetch('https://api.example.com/products', {
  next: { revalidate: 60 }
});

// 无缓存,每个请求都新鲜(SSR 行为)
const data = await fetch('https://api.example.com/user/profile', {
  cache: 'no-store'
});

// 使用标签缓存以便按需重新验证
const data = await fetch('https://api.example.com/products/123', {
  next: { tags: ['product-123'] }
});

段级缓存

你可以在单个页面中混合渲染策略:

// 静态布局(在构建时缓存)
export default function Layout({ children }) {
  return <div><Nav />{children}<Footer /></div>;
}

// 动态页面(每个请求都新鲜)
export const dynamic = 'force-dynamic';
export default async function Page() { /* ... */ }

缓存何时变得棘手

真正的陷阱:如果路由段中的任何组件使用动态函数(cookies()headers()searchParams),整个段变为动态。一个深层嵌套 Server Component 中的一个未缓存的 fetch 使整个页面动态化。

这在生产中坑了我们。我们有一个应该是 ISR 缓存的产品页面,但深层嵌套的 RecentlyViewed 组件在读取 cookies。整个页面变为动态,TTFB 从 50ms 跳到 400ms,我们两周都没注意到。

修复方法:在 Suspense 边界后隔离动态组件,或将它们移到在客户端获取的 Client Components。

决策框架:何时使用每一种

迁移三个生产应用后,这是我使用的决策框架。它不是关于"SSR 对 RSC",而是关于"为哪个组件选择哪种渲染策略"。

使用 Server Components(默认)的情况:

  • 组件显示数据但不需要交互性
  • 你在使用仅服务器资源(DB、文件系统、私有 API)
  • 组件导入庞大的库(markdown 解析器、语法高亮器)
  • SEO 对内容很重要(搜索引擎获取完整 HTML)
  • 内容可以静态分析或缓存

使用 Client Components 的情况:

  • 你需要 useStateuseEffectuseRef 或其他 React hooks
  • 你需要浏览器 API(localStorage、地理定位、IntersectionObserver)
  • 你需要事件处理程序(onClick、onChange、onSubmit)
  • 你在使用需要浏览器上下文的第三方库
  • 你需要实时更新(WebSockets、轮询)

使用 SSR(force-dynamic)的情况:

  • 内容是每个用户/会话个性化的
  • 数据变化太频繁无法进行 ISR
  • 你需要请求时间信息(认证状态、地理位置标头)
  • SEO 仍然需要服务器渲染的 HTML

使用静态生成的情况:

  • 内容变化不频繁(营销页面、文档、博客文章)
  • 性能至关重要(在 CDN 边缘缓存)
  • 所有用户的内容相同

对于我们的 Next.js 开发项目,我们通常最终得到大约这样的分割:60% Server Components(静态),20% Server Components(动态/SSR),15% Client Components,以及 5% 使用 Suspense 边界的混合模式。

从 Pages Router 迁移的模式

如果你在迁移现有的 Next.js 应用,不要尝试一次转换所有内容。我见过那样的失败。这是有效的增量方法:

阶段 1:共存

Next.js 16 同时支持 pages/app/ 目录。在 app/ 中开始新路由,让现有的保持原样。

阶段 2:布局迁移

首先迁移你的布局。_app.tsx_document.tsx 变为 app/layout.tsx。这通常是最简单的胜利 — 布局是完美的 Server Components。

阶段 3:静态页面优先

迁移你最简单的静态页面。营销页面、关于页面、博客文章。这些是简单的 Server Component 转换。

阶段 4:动态页面

转换使用 getServerSideProps 的页面。这是你会遇到最多摩擦的地方,特别是围绕数据获取模式和认证。

阶段 5:客户端交互性

将交互式岛屿提取到 Client Components 中。这是最难的部分 — 你需要识别最小的客户端边界。

// 之前:Pages Router 中默认所有内容都是"客户端"
// 之后:显式边界

// app/products/[id]/page.tsx (Server Component)
export default async function ProductPage({ params }) {
  const product = await getProduct(params.id);
  return (
    <article>
      <h1>{product.name}</h1>
      <ProductGallery images={product.images} /> {/* Client */}
      <div dangerouslySetInnerHTML={{ __html: product.description }} /> {/* Server */}
      <PricingWidget product={product} /> {/* Client */}
      <Suspense fallback={<Skeleton />}>
        <RelatedProducts categoryId={product.categoryId} /> {/* Server */}
      </Suspense>
    </article>
  );
}

如果你需要帮助规划迁移策略,我们的团队已经做过足够多次,知道地雷在哪里 — 联系我们,我们可以讨论你的具体架构。

技术 SEO 影响

经过 12+ 年的观察搜索引擎如何处理 JavaScript 渲染,我可以告诉你:RSC 模型是自 SSR 本身以来对技术 SEO 最好的事情。

原因如下:

Server Components 在服务器上呈现完整的 HTML。 Googlebot 获得完整内容,无需执行任何 JavaScript。这不是新的 — SSR 也做到了。但 RSC 通过显著较少的客户端 JavaScript 来做到这一点,这直接影响 Core Web Vitals。

Google 已确认 INP(Interaction to Next Paint)自 2024 年 3 月以来是排名信号。我们的生产数据显示 RSC 重页面的 INP 得分比等效 SSR 页面高 47%。更少的 JavaScript = 更少的主线程竞争 = 更好的 INP。

流式处理影响爬虫行为。 Googlebot 自 2023 年以来支持 HTTP 流式处理,但它有一个超时。如果你最慢的 Suspense 边界需要 15 秒,Googlebot 可能不会等待它。将关键 SEO 内容保持在 Suspense 边界之外,或确保你的 suspense fallbacks 包含有意义的内容。

对于 SEO 是主要关注的客户,我们经常推荐我们的 headless CMS 开发方法,配合 App Router — 内容驻留在 CMS 中,通过 Server Components 渲染,并向浏览器发送零个不必要的 JavaScript。这是搜索性能的所有方面中最好的。

Astro 也值得考虑,如果你的网站主要是内容驱动的,交互性最少。但对于具有丰富交互功能的应用,Next.js 16 与 RSC 达到了最佳点。

常见问题

Next.js 16 中 SSR 和 RSC 有什么区别? SSR(服务器端渲染)是一种渲染策略,决定你的页面 HTML 何时生成 — 在服务器上的每个请求。React Server Components(RSC)是一种组件类型,决定哪些代码发送到浏览器。在 App Router 中,它们一起工作:RSC 定义哪些需要客户端 JavaScript,SSR 处理 HTML 生成。你通常同时使用两者。

React Server Components 会取代服务器端渲染吗? 不会。RSC 和 SSR 是互补的,不是竞争的。在 Next.js 16 的 App Router 中,每个页面都为初始 HTML 响应使用 SSR。RSC 决定该页面中哪些组件需要发送 JavaScript 到客户端以进行水合。你可以有一个完全 SSR 的页面,完全由 Server Components 组成(无客户端 JS),或两者的混合。

React Server Components 减少多少包大小? 在我们的生产测量中,基于 RSC 的 App Router 页面的 JavaScript 包比等效的 Pages Router 实现平均小 63%。节省取决于你的组件树 — 有大量仅显示内容的页面会看到最大的收益,而高度交互的页面(仪表板、编辑器)会看到更小的改进。

我应该将现有的 Next.js 应用迁移到 App Router 吗? 这取决于你的痛点。如果你的 Core Web Vitals 因为大型 JavaScript 包而受到影响,或者如果你的 TTFB 因为顺序数据获取而很高,迁移是值得的。如果你的 Pages Router 应用性能良好,你的团队效率高,就没有紧迫感。Next.js 同时支持两个路由,所以你可以增量迁移。

缓存如何在 Next.js 16 的 Server Components 中工作? Next.js 16 大大简化了缓存模型。Server Components 可以被静态缓存(对静态数据的默认),基于时间重新验证(ISR),或为每个请求呈现新鲜。你在 next: { revalidate } 的 fetch 级别或在 export const dynamic 的路由段级别控制这一点。小心:段中的一个动态函数使整个段动态化。

Server Components 影响 SEO 吗? Server Components 对 SEO 很好。它们在服务器上呈现完整的 HTML,搜索引擎可以在不执行任何 JavaScript 的情况下索引。此外,减少的客户端 JavaScript 改进 Core Web Vitals 得分,特别是 INP 和 TTI,这是排名信号。一个注意事项是 Suspense 边界内的内容以渐进方式流式传输,所以确保关键 SEO 内容不在缓慢数据获取后面。

我可以将 React Server Components 与 headless CMS 一起使用吗? 当然 — 这是最好的配对之一。Server Components 可以直接在组件级获取 CMS 内容,而不将 API 密钥或 CMS SDK 代码暴露给客户端。像 Contentful SDK、Sanity 客户端或 Prismic 的 @prismicio/client 这样的库完全停留在服务器上。结合 ISR 或通过 webhook 的按需重新验证,你获得快速、可缓存的页面,零个不必要的客户端 JavaScript。

在生产中使用 RSC 的最大陷阱是什么? 我遇到的三个最大问题:(1) 嵌套 Server Components 中的意外瀑布流数据获取 — 使用 React DevTools 和服务器计时标头进行分析和修复。(2) 通过在嵌套组件中使用 cookies()headers() 来意外使缓存的页面动态化。(3) 将非序列化数据(函数、类实例、日期)从 Server 传递到 Client Components 时的 prop 序列化错误。早期建立良好的 linting 规则和组件边界约定。