Next.js 16 中的 SSR 与 RSC:生产决策指南
关于 SSR 与 RSC 的讨论已经被炒作、不完整的心智模型以及老实说一些令人困惑的文档所混淆。它们不是竞争技术 — 它们是在应用的不同层解决不同问题的互补工具。但知道在特定场景中应该使用哪种工具?这是真正的工程判断所在。
让我带你了解我学到的所有内容,附带真实的生产数据、实际的代码模式,以及在会议演讲中没人谈论的权衡。
目录
- 理解基础知识
- SSR 在 Next.js 16 中的工作原理
- React Server Components 的工作原理
- 性能对比:真实生产数据
- 包大小影响
- 流式处理和瀑布流模式
- 实际可行的缓存策略
- 决策框架:何时使用每一种
- 从 Pages Router 迁移的模式
- 技术 SEO 影响
- 常见问题

理解基础知识
在深入细节之前,让我们建立一个清晰的心智模型。这比你想的更重要 — 我见过资深工程师因为术语重叠而混淆 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} />;
}
渲染管道如下所示:
- 请求命中服务器
- Next.js 从上到下执行 RSC 树
- Server Components 解决其异步操作(数据获取等)
- 渲染的 RSC 负载被序列化
- SSR 将其转换为初始响应的 HTML
- 客户端接收 HTML + RSC 负载 + Client Component JS
- React 仅对 Client Component 边界进行水合
步骤 3-6 可以通过流式处理进行,我将在下面详细讨论。
React Server Components 的工作原理
RSC 不仅仅是"在服务器上运行的组件"。它们代表一种根本不同的执行模型。
当 Server Component 渲染时,其输出是 UI 的序列化描述 — 类似于 JSON 型树结构。此负载包括 Server Components 的渲染输出(作为 HTML 型节点)和对 Client Components 的引用(作为模块指针加上其序列化的 props)。
这意味着:
- Server Components 可以直接访问数据库、文件系统和仅服务器 API
- 它们可以在组件级使用
async/await - 它们的代码、依赖项和导入永远不会出现在客户端包中
- 它们不能使用
useState、useEffect或任何浏览器 API - 它们不能将函数作为 props 传递给 Client Components(函数无法序列化)
最后一点经常让人困惑。你不能这样做:
// ❌ 这会抛出错误
async function ServerParent() {
const handleClick = () => console.log('clicked');
return <ClientChild onClick={handleClick} />;
}
你需要将处理程序移到 Client Component 本身,或使用 Server Actions。

性能对比:真实生产数据
在从 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-fns、marked、sanitize-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 的情况:
- 你需要
useState、useEffect、useRef或其他 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 规则和组件边界约定。