跳转到主要内容

SSR 与 RSC

当你需要更快首屏、更好的 SEO 或更低客户端 bundle 时,渲染策略会直接决定架构取舍。

关键的变化是:你不再只是选择“React 在哪渲染”,而是在选择边界——什么在服务端运行、什么在客户端运行、以及数据如何跨越边界。

迁移建议

迁移失败往往源于“从叶子开始改”。建议从路由边界入手,再把纯服务端工作逐步下沉。

  • 先把路由级数据加载、`loading`/`error` 边界搭稳。
  • 再逐步把“只在服务端需要”的计算与数据拼装下沉到服务端。

能力矩阵(怎么选)

把 `SSR`/`Streaming`/`RSC` 当成不同成本结构的工具。没有绝对“最好”,只有是否匹配你的目标与团队约束。

先用这张表选一个基线方案,再结合 profiling 与体验要求迭代细化。

Mode首屏客户端体积数据访问复杂度
CSR偏慢(依赖网络 + JS)偏大仅客户端
SSRHTML 首屏更好与 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` 与错误边界齐全,并提供可操作的恢复路径。
  • 缓存策略明确:哪些缓存、在哪里缓存、如何失效。

延伸阅读

如果你想继续深挖,可以按“数据边界 → 组件边界 → 底层模型”的顺序读下去:先把实践做对,再补齐原理。

SSR 与 RSC - Guides - React 文档 - React 文档