SSR 与 RSC
当你需要更快首屏、更好的 SEO 或更低客户端 bundle 时,渲染策略会直接决定架构取舍。
关键的变化是:你不再只是选择“React 在哪渲染”,而是在选择边界——什么在服务端运行、什么在客户端运行、以及数据如何跨越边界。
迁移建议
迁移失败往往源于“从叶子开始改”。建议从路由边界入手,再把纯服务端工作逐步下沉。
- 先把路由级数据加载、`loading`/`error` 边界搭稳。
- 再逐步把“只在服务端需要”的计算与数据拼装下沉到服务端。
能力矩阵(怎么选)
把 `SSR`/`Streaming`/`RSC` 当成不同成本结构的工具。没有绝对“最好”,只有是否匹配你的目标与团队约束。
先用这张表选一个基线方案,再结合 profiling 与体验要求迭代细化。
| Mode | 首屏 | 客户端体积 | 数据访问 | 复杂度 |
|---|---|---|---|---|
| CSR | 偏慢(依赖网络 + JS) | 偏大 | 仅客户端 | 低 |
| SSR | HTML 首屏更好 | 与 CSR 接近(仍需 hydration) | 服务端 + 客户端 | 中 |
| Streaming SSR | 体感最好 | 类似 SSR | 服务端 + 客户端 | 中-高 |
| RSC | 好(非交互部分更少 JS) | 可显著更小 | 服务端优先,客户端孤岛 | 高(边界纪律) |
边界:序列化与安全
边界既是性能工具,也是安全工具。把它当作 `API`:显式、可校验、可演进。
- 服务端代码可访问密钥与内网资源;绝不能泄露到客户端。
- 跨边界只传可序列化数据;把“胖对象”和连接留在服务端。
- 把边界当作 `API` 合同:校验输入,必要时清洗输出。
Hydration 成本:把交互收敛到孤岛
`SSR` 仍可能慢:如果客户端要 `hydrate` 很大的树,交互就会卡。优先“结构在服务端,交互在客户端”。
落到实践里就是:内容主体尽量在服务端完成渲染;把输入、切换、导航等交互收敛到小的客户端孤岛。
TypeScript
// Server-side (route/page boundary)
export default async function Page() {
const res = await fetch('https://example.com/api/products', { cache: 'no-store' });
const products = await res.json();
return (
<div>
<h1>Products</h1>
<ClientFilters initialCount={products.length} />
<ul>{products.map((p: any) => <li key={p.id}>{p.name}</li>)}</ul>
</div>
);
}
// Client-side (interactive island)
// 'use client';
function ClientFilters({ initialCount }: { initialCount: number }) {
// useState / useEffect / event handlers live here
return <div>Count: {initialCount}</div>;
}常见坑
- 没有清晰边界地混用“客户端状态 UI”和“服务端数据逻辑”,导致心智负担与 bug。
- 把 `SSR` 当成性能银弹;网络瀑布与过重的 `hydration` 仍会拖慢体验。
上线检查清单
- 敏感数据只留在服务端;客户端 bundle 不包含密钥。
- `hydration` 范围有设计;大段非交互内容尽量不下发客户端 JS。
- 路由级 `loading` 与错误边界齐全,并提供可操作的恢复路径。
- 缓存策略明确:哪些缓存、在哪里缓存、如何失效。
延伸阅读
如果你想继续深挖,可以按“数据边界 → 组件边界 → 底层模型”的顺序读下去:先把实践做对,再补齐原理。
- 继续读:数据获取(Guides),把缓存边界、瀑布与失效策略讲清楚。
- 想从概念到实践完整走一遍:服务端组件(Learn)。
- 想理解“为什么要这样划边界”:Server Components(原理),以及 并发与 Suspense(原理)。
- 查 API 细节与边界:Suspense(API 参考)。