RSC 模型

服务端组件的执行模型与设计动机
0 JS
服务端组件 bundle 大小
60-70%
首字节时间下降
Parallel
数据获取与渲染
LLP
服务端/客户端协议

核心设计原则

React Server Components(RSC)将组件树的执行分布到服务端和客户端两端,服务端负责数据获取与渲染,客户端负责交互。这种分离改变了传统的" hydrated HTML + JS bundle"范式。

  • 服务端组件(Server Component):在服务端执行,可直接访问后端数据源(数据库、文件系统、第三方 API),不参与客户端水合,产物仅为序列化后的 React Element Tree。
  • 客户端组件(Client Component):通过 'use client' 声明,在客户端执行,可使用 useState、useEffect 等 Hook,完整参与水合流程。
  • 水合边界(Hydration Boundary):Server Component 可嵌入 Client Component,但 Client Component 无法嵌套 Server Component——通过 children prop 或 slot 模式实现组合。
  • 零客户端 bundle 收益:未被标记为 Client Component 的子树,不会出现在客户端 JS bundle 中。对于 CMS、数据密集型页面,可削减 40-70% 的 JS 体积。
// 服务端组件:直接访问数据库,无需 API Layer
// app/posts/page.server.tsx
async function PostsPage() {
    // 直接调用数据库,无 HTTP 开销
    const posts = await db.query('SELECT * FROM posts ORDER BY created_at DESC LIMIT 20');
    
    return (
        <div className="posts-grid">
            {posts.map(post => (
                {/* PostCard 仍为 Server Component */}
                <PostCard key={post.id} post={post} />
            ))}
        </div>
    );
}

// Client Component:负责交互逻辑
// components/interactive-search.client.tsx
'use client';
import { useState } from 'react';

export function InteractiveSearch({ allPosts }) {
    const [query, setQuery] = useState('');
    // 客户端过滤,数据已经在 props 中传递过来
    const filtered = allPosts.filter(p => p.title.includes(query));
    
    return (
        <div>
            <input onChange={e => setQuery(e.target.value)} />
            {filtered.map(p => <PostCard key={p.id} post={p} />)}
        </div>
    );
}

数据瀑布消解

Server Component 支持 async/await,可并行发起多个数据请求。A 组件的渲染不阻塞 B 组件的数据获取,整体延迟等于最长路径而非累加和。

代码分割粒度

传统 Code Splitting 以路由为粒度,RSC 以组件为粒度。共享的低交互组件(如 Header、Footer、Nav)仍可服务端渲染,高交互组件单独 Client Bundle。

安全边界

服务端组件的闭包不会泄露到客户端。数据库凭证、密钥、敏感逻辑天然隔离在服务端执行环境,无法被客户端 JS 获取。

服务端/客户端边界

'use client' 指令与水合边界管理

边界指令模型

RSC 的边界由 'use client' 指令显式声明。没有该指令的组件默认服务端执行。边界一旦声明,其下游子树全部视为客户端代码——即使下游子组件未声明 'use client'

  • 文件级指令:整个模块文件的所有组件均为 Client Component
  • 传递性规则:Client Component 的 children prop 若传入 Server Component,该 Server Component 在运行时属于客户端环境
  • 边界越少越好:过碎的边界会导致 RSC 序列化开销增加,React 团队建议以"功能岛屿"(Islands)模式划分
// ✅ 正确:Composition Pattern(组合模式)
// Server Component 持有 Client Component 作为 children prop
async function DashboardPage() {
    const data = await fetchDashboardData(); // 服务端获取数据
    
    return (
        <Layout>
            {/* Chart 是 Client Component,数据通过 props 传入 */}
            <Chart data={data.chartSeries} />
        </Layout>
    );
}

// ❌ 错误:Client Component 内部 import Server Component
'use client';
import { ServerComponent } from './server-component'; // 运行时错误!

// ✅ 正确:通过 children prop 传递 Server Component
function ClientWrapper({ children }) {
    const [state, setState] = useState(0);
    return <div onClick={() => setState(s => s+1)}>{children}</div>;
}

// 使用:Server Component 作为 children 传入 Client Component
<ClientWrapper>
    <ServerComponent />  {/* 这是合法的 composition */}
</ClientWrapper>

Prop 序列化约束

Server Component → Client Component 的 props 必须可序列化。允许的类型:JSON 原生类型、Array、Object、Date(转为 string)、React Element(需通过 slot pattern)。禁止:函数、Symbol、undefined、class 实例。

  • Event Handler 必须在 Client Component 内部定义,无法作为 props 传递
  • 大型数据集建议通过 URL params 或专门的 RSC route 传递,避免 props 膨胀
  • next/link 和 next/image 等 Next.js 内置组件已预置为 Client Component

Context 隔离

React Context 无法在 Server Component 中使用——Context 是运行时机制,服务端无水合概念。Client Component 的 Context Provider 必须在 'use client' 模块顶层。

  • Theme Context、Auth Context 需要在 Client Component root 注入
  • 服务端获取的数据通过 props drilling 或专门的 slot 模式传入 Client Component
  • RSC 的设计鼓励"数据获取在服务端,状态在客户端"的单向数据流
架构洞察

Next.js App Router 的路由节点默认都是 Server Component。只有 Leaf Node(交互区域)才需要标记 'use client'。这种模式与"发送 HTML + JS 用于交互"的传统 SSR 有本质区别——RSC 发送的是 React Element Tree 的序列化协议,客户端按需执行。

组件树序列化

RSC Payload 格式与 wire protocol
JSON
Element Tree 编码格式
Chunked
流式编码单元
V8
序列化性能保障
1:1
Element 映射比

RSC Wire Protocol

Server Component 树的执行结果不是 HTML,而是 RSC Payload——一个包含 React Element 节点、Slot 引用、Client Reference 标记的二进制编码流。客户端 React 根据此 Payload 重构组件树。

  • Identifier Format:每个模块有唯一 ID(如 $id="4"),Client Reference 标记为 $旺季="client"
  • Slot Mechanism:placeholder 用于在服务端组件树中嵌入未解析的客户端子树引用
  • Lazy Reference:延迟加载的模块以 <...> 标记,客户端根据需要动态 import
  • Streaming Chunk:每个 Chunk 包含部分序列化的组件树,支持渐进式 flush
// RSC Wire Protocol 示例(简化格式)
// 服务端返回的 RSC Payload 看起来类似:
// 0:DServerFragment,["$","div",null,{"className":"posts-grid"}]
// J0:["$","article",null,{"id":"1","title":"..."}]
// 1:DServerFragment,["$","div",null,{"children":[null]}]

// Client Reference 标记(不会被服务端解析)
// ["$","Chart",null,{"data":{"$undefined":null}}]

// Next.js App Router 中的实际调用
// request.toRSCStream() → ReadableStream<Uint8Array>

// 服务端实现
export async function GET(request: Request) {
    const tree = await renderToStream(<App />);
    // Content-Type: text/rsc
    return new Response(tree, {
        headers: { 'Content-Type': 'text/rsc' }
    });
}

React Flight 协议

React Flight 是 RSC 的底层传输协议。它将 React Element 树编码为可流式传输的 JSON-like 格式,支持中断恢复(resumability)。Next.js App Router 建立在 Flight 之上。

模块图谱(Module Graph)

RSC 的 Client Reference 形成模块图谱边界。服务端模块解析后,将 Client Component 的 import 以 Reference 形式嵌入序列化流,客户端按 graph 懒加载。

安全属性剥离

序列化时,React 会剥离危险属性(如 onClick 以事件处理器)。序列化流中不包含可执行代码,仅包含数据结构与引用标记。

流式渲染

Suspense 与渐进式 HTML 注入

流式 SSR vs 传统 SSR

传统 Server-Side Rendering 必须等待完整组件树渲染完成才能发送 HTML——数据获取的瓶颈导致 TTFB 延迟累加。RSC 的流式渲染允许已解析的部分立即 flush,未解析的部分以 Suspense fallback 占位。

  • deferred.js:Next.js 13.1+ 引入的 defer 语法,将非关键数据获取推迟到服务端渲染完成后
  • Streaming HTML:使用 Transfer-Encoding: chunked,分块发送已渲染的 HTML shell
  • Progressive Hydration:客户端按 DOM 顺序渐进式水合,先出现的区块先交互
// Next.js App Router 流式渲染示例
// app/products/page.tsx
import { Suspense } from 'react';
import { defer } from '@remix-run/node';

async function ProductsPage({ searchParams }) {
    // 关键数据:同步获取,影响 FCP
    const categories = await getCategories();

    // 非关键数据:defer 推迟,服务端渲染不阻塞
    const productsPromise = getProducts({ 
        category: searchParams.category 
    });

    return (
        <div>
            <CategorySidebar categories={categories} />
            {/* ProductGrid 使用 defer 后的 Promise */}
            <Suspense fallback={<ProductSkeleton />}>
                <ProductGrid productsPromise={productsPromise} />
            </Suspense>
        </div>
    );
}

// ProductGrid 消费 Deferred 数据
// 使用 use() hook(React 19+)或 await inside component
async function ProductGrid({ productsPromise }) {
    // 直接 await 不阻塞:React 会等待数据后继续渲染
    const products = await productsPromise;
    return products.map(p => <ProductCard key={p.id} product={p} />);
}

Suspense 边界策略

Suspense boundary 的划分决定流式渲染的粒度。过粗的边界会导致延迟叠加(整块等待),过细的边界会导致 RSC chunk 碎片化。

  • 路由级 Suspense:最粗粒度,整个页面作为单一 chunk
  • 区域级 Suspense:推荐,按功能区块划分(Sidebar、Content、Related)
  • 组件级 Suspense:最细,用于数据获取组件(CommentThread、Reviews)

流式 HTML 注入顺序

服务器按组件树顺序 flush 已解析的部分。关键路径组件(Above-the-fold)应放在 Suspense 边界之外,确保首屏内容尽快到达客户端。

  • Layout、Navigation 等框架级组件先 flush
  • 主要内容区块按数据依赖顺序 flush
  • 页脚、推荐等低优先级区域可包裹在深层 Suspense 中
性能收益

流式渲染可使 TTFB 降低 200-500ms,尤其在数据源延迟差异大的场景(如 Product 页面需要类别、推荐、库存三个 API 串联)。传统 SSR 需要等待三者全部返回,而 RSC 流式可让类别和导航先到达浏览器。

缓存策略

Router Paging 与 revalidate 机制
Full
Route Segment 缓存
ISR
增量静态再生成
Tag-based
缓存失效机制
Unlimited
服务端 KV 缓存

多层级缓存架构

Next.js App Router 实现四级缓存,从服务端到客户端依次递增最近的层级优先命中。

  • Router Cache(内存):客户端内存中的 RSC Payload 缓存,按 segment 存储,支持后退/前进导航。生命周期由 SWR 策略控制。
  • Data Cache(持久化):服务端 KV(如 Redis、Vercel KV)中的 fetch 结果缓存,生命周期由 revalidate 或 fetch cache 控制。
  • Full Route Cache(磁盘):整个路由的渲染结果序列化到磁盘,仅在静态渲染路径下可用。
  • Static Generation(构建时):Build 时生成的静态 HTML,可部署到 CDN。
// Next.js 缓存控制策略

// 1. Data Cache: fetch 级别缓存控制
const data = await fetch('https://api.example.com/products', {
    // next.revalidate: 增量再生成间隔(秒)
    next: { revalidate: 3600 }  // 1小时后重新生成
});

// 2. Route Segment 级别缓存配置
// app/products/layout.tsx
export const revalidate = 60;  // 整段路由每60秒重新验证

// 3. 缓存标签:按 tag 失效特定数据源
// 写入时打标签
await fetch('https://api.example.com/products', {
    next: { tags: ['products', 'catalog'] }
});

// 触发失效
// revalidateTag('products')  // Vercel 或 Next.js 缓存 API

// 4. 动态路由:force-dynamic 强制实时渲染
export const dynamic = 'force-dynamic';
// 不使用缓存,每次请求实时渲染(SSR-like 行为)

Router Cache 策略

客户端 Router 缓存 RSC Payload,支持 prefetch 预加载与 soft navigation。用户在客户端导航时,服务端仅返回缺失的 segment diff,而非完整页面。

Soft Navigation

App Router 的客户端导航优先使用缓存(soft),仅有明确数据变更(如用户操作导致的写)才走服务端(hard navigation)。这大幅提升了 SPA 般的交互流畅度。

On-Demand Revalidation

通过 revalidateTag()revalidatePath() API,在 CMS 更新、订单状态变更等事件时主动失效缓存,而非依赖时间窗口。

架构决策

RSC 的缓存设计将"页面是否需要实时数据"的选择权交给开发者。Dynamic Route(库存、价格、用户数据)使用 force-dynamic;CMS 内容、配置数据使用 ISR + on-demand revalidate;静态资源使用 build-time static generation。混合使用是生产环境的标准实践。