引言
React 18 引入的 Server Components 和 Next.js 13+ 的 App Router 标志着前端架构的一次范式转变:数据依赖与渲染逻辑不再局限于浏览器端,而是在服务端与客户端之间的边界被重新定义。这不是 SSR 的简单升级——而是对组件、数据流与网络边界的一次重新思考。
一、从 Client Components 到 Server Components
1.1 核心概念澄清
在传统的 React 应用中,所有组件都在浏览器中执行。服务端只负责生成初始 HTML(SSR),然后 JavaScript 包被下载后「注水」(Hydrate),使应用可交互。这意味着组件树的每一个字节——包括那些从不需要交互的展示型组件——都被打包并传输到客户端。
Server Components 从根本上改变了这一模型:默认情况下,组件在服务器上执行,其输出以特殊的 React Server Payload 格式传输到浏览器。组件代码本身不会被打包到客户端 bundle 中。
// app/posts/[id]/page.tsx - Next.js App Router 中的 Server Component
// 默认即为服务器组件,可以异步执行数据库查询
import { notFound } from 'next/navigation';
import { prisma } from '@/lib/db';
import LikeButton from './LikeButton'; // 客户端组件
async function getPost(id: string) {
return await prisma.post.findUnique({
where: { id },
include: { author: true, tags: true }
});
}
export default async function PostPage({ params }: { params: { id: string } }) {
const post = await getPost(params.id);
if (!post) notFound();
return (
{post.title}
By {post.author.name}
{/* 客户端交互岛 */}
);
}
// 注意:此文件不会被打包到客户端 JS 中
1.2 零字节打包(Zero-Bundle-Size)
Server Components 的最大优势是零字节打包:服务器组件中引用的任何 npm 包——比如 markdown 解析器、csv 处理库、甚至是数据库驱动——都不会出现在客户端 bundle 中。这意味着组件可以安全地导入大型库,而不会影响首屏加载性能。
这是以往任何前端框架都无法做到的特性。在传统模式下,即使展示型组件只是读取 JSON 并渲染,其引用的 lodash 之类的库也会被打包(除非做代码分割)。
二、App Router 带来的范式转变
2.1 文件系统路由的进化
Next.js Pages Router 使用 pages/ 目录的简单文件路由。App Router 引入了 app/ 目录,带来了嵌套布局(Nested Layouts)、Loading UI、Error Boundaries、Parallel Routes 和 Intercepting Routes 等新概念。
最重要的是,每个路由可以声明自己的 loading.tsx、error.tsx 和 notFound.tsx,这些文件会作为独立边界的 fallback UI 被 Suspense 包裹——实现了「局部加载」而非「全局加载」。
app/
├── layout.tsx // 根布局(所有页面共享)
├── page.tsx // 首页
├── loading.tsx // 全局加载中 UI
├── error.tsx // 全局错误边界
├── blog/
│ ├── layout.tsx // 博客子树专属布局
│ ├── page.tsx
│ ├── loading.tsx // 博客列表专属加载 UI
│ └── [slug]/
│ ├── page.tsx
│ └── error.tsx // 文章详情页专属错误处理
└── dashboard/
├── @analytics/ // Parallel Route
├── @charts/
└── page.tsx
2.2 服务端 Actions 与表单处理
App Router 引入了 Server Actions——直接在服务器组件中定义异步函数,可被表单直接调用。这消除了传统 SPA 中需要手动编写 API 端点的繁琐。
// Server Action(定义在服务器组件中或独立文件标记 'use server')
// app/posts/new/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
import { prisma } from '@/lib/db';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
await prisma.post.create({ data: { title, content } });
revalidatePath('/posts'); // 使缓存失效,触发增量再渲染
}
// app/posts/new/page.tsx - 在表单中直接使用 Server Action
import { createPost } from './actions';
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" required />
<textarea name="content" required />
<button type="submit">发布</button>
</form>
);
}
Server Actions 自动处理序列化、CSRF 防护、乐观更新(与 useFormStatus/useFormState 集成)和缓存失效。这是一个端到端的数据变更模型,无需 REST 或 GraphQL 的样板代码。
三、流式渲染与 Suspense 边界
3.1 Selective Hydration(选择性注水)
App Router 使用 Streaming SSR 和 Selective Hydration 实现渐进式可交互。服务器按 Suspense 边界分段发送 HTML 和 React Payload,浏览器优先注水可见区域的组件,而屏幕外的组件延迟注水。
与 Next.js Pages Router 的 blocking SSR(必须等所有数据加载完成才能发送首字节)相比,Streaming SSR 的首字节时间(TTFB)更短,可交互时间(TTI)也更优。
3.2 use() Hook:组件级数据获取
React 的 use() Hook(非正式提案,已在 Next.js 14+ 中可用)允许在任意深度组件中直接读取 Promise,而非只能在 useEffect 或 getServerSideProps 中执行。
// 使用 use() Hook 在组件内直接获取数据
import { use } from 'react';
// Suspense 边界包裹的子组件
function Comments({ commentsPromise }: { commentsPromise: Promise }) {
const comments = use(commentsPromise); // 在渲染中暂停,直到 Promise 完成
return (
{comments.map(comment => (
- {comment.text}
))}
);
}
// 父组件将 fetch 结果传递给子组件
function PostPage({ postPromise, commentsPromise }) {
return (
加载中...}>
加载评论中...}>
);
}
这与传统的「props 瀑布」完全不同——数据获取请求在渲染时被触发,而不需要在父组件中预取后层层传递。
四、缓存策略的重新定义
App Router 提供了四层缓存体系:
- Request Memoization:同一渲染周期内相同参数的子请求自动去重(类似 React Query 去重)
- Data Cache:长期持久化到文件/Runtime Memory 的查询结果缓存(跨请求共享)
- Full Route Cache:基于 Data Cache 的最终 HTML/Payload 缓存
- Router Cache:浏览器端的内存缓存(控制 Prefetch 的过期时间)
通过 Route Segment Config(如 export const dynamic = 'force-dynamic'),可以精确控制每个路由的缓存行为。revalidatePath 和 revalidateTag 允许在数据变更时使特定缓存失效,实现类似 ISR(增量静态再生)但更精细的缓存策略。
五、适用场景与边界
Server Components 并非万能。当以下需求时仍需 Client Components:
- 事件处理(onClick、onSubmit)
- 浏览器 API 访问(localStorage、navigator、IntersectionObserver)
- React Hooks 依赖(useState、useEffect、useReducer)
- 浏览器端实时协作(WebSocket / WebRTC)
正确的方法论是:默认使用 Server Components,仅在必要时通过 'use server' 或 'use client' 指令明确标记边界。这被称为「服务端优先」(Server-first)的开发心态。
六、总结
React Server Components 与 App Router 不是对传统 SPA 的否定,而是对过度客户端化的一次纠偏。它将数据获取、计算密集型渲染和后端交互回归到更高效的服务器环境,同时保留了客户端提供丰富交互的能力。对于追求首屏性能和开发效率的 SSR 项目,App Router 已成为当前最完整的解决方案——但也需要开发者重新理解组件、缓存和数据流的心智模型。

发表评论 取消回复