React Server Components (RSC) 原理剖析:流式渲染的终极形态

6 阅读4分钟

React 18 带来的 Server Components (RSC)  被誉为前端架构的“范式转移”。它如何做到“零 Bundle Size”?如何实现真正的流式 SSR?

今天,我们从网络传输协议组件序列化的视角,深度剖析 RSC 的底层实现机制。

1. 传统 SSR 的痛点

在 RSC 出现之前,我们主要使用 Next.js 的 getServerSideProps 或 Nuxt 的服务端渲染:

问题描述
水合开销大客户端需要重新执行一遍组件逻辑来绑定事件
Bundle 臃肿即使只在服务端运行的库(如 Markdown 解析器)也会打入客户端包
瀑布请求服务端获取数据 → 渲染 HTML → 客户端加载 JS → 水合

2. RSC 的核心突破:组件即数据

RSC 的本质是:服务端将组件树序列化为一种特殊的 JSON 格式(RSC Payload),通过流发送给客户端。

2.1 RSC 协议格式

M1:{"id":"./src/App.js","chunks":["client1"],"name":"default"}
J0:["$","div",null,{"children":[["$","h1",null,"Hello"],["$","p",null,"World"]]}]

关键标识符

  • M:模块引用(Module Reference)
  • J:JSON 节点(Element)
  • S:字符串(String)
  • $:React 元素标记

2.2 零 Bundle Size 的秘密

// 这个组件只在服务端运行
import { marked } from 'marked'; // 巨大的 Markdown 库

function Article({ content }) {
    const html = marked(content); // 服务端直接转为 HTML
    return <div dangerouslySetInnerHTML={{ __html: html }} />;
}

优势marked 库的代码永远不会发送到客户端,因为它在服务端已经执行完毕并转为了静态 HTML。

3. 流式渲染(Streaming SSR)

3.1 Suspense 与流的协作

RSC 允许服务端在数据还没准备好时,先发送页面的其他部分:

function Page() {
    return (
        <div>
            <Header /> {/* 立即发送 */}
            <Suspense fallback={<Loading />}>
                <Comments /> {/* 等待数据库查询完成后发送 */}
            </Suspense>
        </div>
    );
}

工作流程

  1. 服务端立即渲染 Header 并推送到流中
  2. 客户端收到 Header 后立即显示
  3. 服务端异步获取 Comments 数据
  4. 数据就绪后,服务端继续推送 Comments 的 Payload
  5. 客户端接收到新内容并替换掉 <Loading />

3.2 核心算法:分块传输

async function renderToStream(Component, props) {
    const stream = new TransformStream();
    const writer = stream.writable.getWriter();
    
    // 异步渲染组件
    const result = await Component(props);
    
    // 分块序列化并写入流
    for await (const chunk of serializeTree(result)) {
        await writer.write(chunk);
    }
    
    await writer.close();
    return stream.readable;
}

4. 客户端与服务端的边界

4.1 "use client" 指令

'use client';

import { useState } from 'react';

export default function Counter() {
    const [count, setCount] = useState(0);
    return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

编译期处理

  • 遇到 "use client",编译器将该组件及其依赖打包为客户端 Bundle
  • 服务端只保留一个对该组件的引用(Reference)

4.2 跨边界传参

// 服务端组件
import ClientCounter from './ClientCounter';

function ServerPage() {
    // ❌ 错误:不能传递函数给客户端组件
    // <ClientCounter onClick={() => {}} />
    
    // ✅ 正确:只能传递可序列化的数据
    return <ClientCounter initialCount={0} />;
}

5. 工业界实战:性能优化策略

5.1 数据库直连

// app/page.js (Server Component)
async function getData() {
    // 直接在组件里查数据库,无需 API 层
    const res = await db.query('SELECT * FROM posts');
    return res.rows;
}

export default async function Page() {
    const posts = await getData();
    return <PostList posts={posts} />;
}

收益:消除了客户端 fetch 数据的往返延迟(RTT)。

5.2 渐进式增强

function ProductPage({ id }) {
    return (
        <>
            <ProductDetails id={id} /> {/* 静态内容,SEO 友好 */}
            <Suspense fallback={<Skeleton />}>
                <UserReviews id={id} /> {/* 动态内容,流式加载 */}
            </Suspense>
        </>
    );
}

6. 面试考点

Q1: RSC 和传统的 SSR 有什么区别?

A: 传统 SSR 发送的是 HTML 字符串,客户端需要重新执行 JS 进行“水合”。RSC 发送的是组件树的序列化数据(RSC Payload),客户端直接根据 Payload 重建 UI,避免了重复执行服务端逻辑,且支持流式传输。

Q2: 为什么 RSC 能减小 Bundle Size?

A: 因为标记为服务端的组件及其依赖库(如日期格式化、Markdown 解析)只在服务端运行,不会打包进客户端的 JavaScript 文件中。

Q3: 服务端组件能使用 useState 吗?

A: 不能。服务端组件是无状态的,它们在渲染完成后就销毁了。如果需要交互状态,必须使用 "use client" 将其转换为客户端组件。

7. 总结

React Server Components 的核心价值:

  1. 更小的体积:服务端库不进入客户端 Bundle
  2. 更快的首屏:直连数据库,减少网络往返
  3. 更好的体验:流式渲染实现渐进式加载
  4. 更简单的架构:消除 API 层,组件即后端

RSC 不是对 SSR 的替代,而是对 React 渲染模型的终极补全。它让前端开发者真正拥有了“全栈”的能力。


如果你觉得这篇关于"React 前沿架构"的文章对你有帮助,欢迎点赞收藏!🚀