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>
);
}
工作流程:
- 服务端立即渲染
Header并推送到流中 - 客户端收到
Header后立即显示 - 服务端异步获取
Comments数据 - 数据就绪后,服务端继续推送
Comments的 Payload - 客户端接收到新内容并替换掉
<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 的核心价值:
- 更小的体积:服务端库不进入客户端 Bundle
- 更快的首屏:直连数据库,减少网络往返
- 更好的体验:流式渲染实现渐进式加载
- 更简单的架构:消除 API 层,组件即后端
RSC 不是对 SSR 的替代,而是对 React 渲染模型的终极补全。它让前端开发者真正拥有了“全栈”的能力。
如果你觉得这篇关于"React 前沿架构"的文章对你有帮助,欢迎点赞收藏!🚀