SSR 中数据获取应该如何设计?
简化版
SSR 数据获取要在服务端渲染前拿到首屏所需数据,并把初始数据注入 HTML,客户端水合时复用这份数据,避免重复请求和水合不一致。还要处理缓存、错误、超时、鉴权和请求隔离。
详细版
SSR 数据获取关注:
- 服务端根据 URL、Cookie、Header 获取数据。
- 首屏必须数据在渲染前准备好。
- 数据序列化注入页面。
- 客户端初始化时复用注入数据。
- 避免客户端再次请求导致闪烁。
- 错误和超时要有兜底。
- 用户相关数据不能被跨请求共享。
缓存策略要区分公共数据和用户私有数据。公共页面可缓存,个性化页面要谨慎。
完整版教学
一、SSR 为什么要提前取数据
SSR 的目标是返回带内容的 HTML。如果服务端不提前取数据,只返回空壳,优势就很小。
因此服务端渲染前需要知道当前路由需要哪些数据,并等待关键数据完成。
二、初始数据注入
服务端拿到数据后,不仅用于生成 HTML,还要把数据安全地注入页面,让客户端水合时复用。
如果客户端水合时重新请求,并且数据不同,就可能出现水合不一致或页面闪烁。
注入数据时要注意序列化安全,避免把用户内容直接变成可执行脚本。
三、请求隔离
SSR 是多用户请求共享同一个服务进程。不能把用户状态放到模块级全局变量里,否则可能出现 A 用户数据串到 B 用户页面的严重问题。
每个请求都应该创建独立上下文,包含 Cookie、Header、用户信息和请求级缓存。
四、面试追问与工程落地
面试官可能问:“SSR 接口慢会怎么样?”
接口慢会拖高 TTFB,用户更晚收到 HTML。解决方式包括缓存、超时降级、并行请求、流式渲染、只阻塞关键数据,非关键数据交给客户端加载。
工程中 SSR 数据层要统一处理超时和错误,否则一个接口慢可能拖垮整个页面。
五、避免瀑布并划分阻塞边界
服务端组件逐层发现请求会形成瀑布:用户接口 200ms 完成后才发商品接口 300ms,总等待约 500ms;若二者互不依赖并行启动,关键数据等待接近 max(200, 300)=300ms,理论上省约 200ms。
async function loadPageData(requestContext, productId) {
const [user, product] = await Promise.all([
getUser(requestContext),
getProduct(productId),
])
return { user, product }
}
| 数据类型 | 是否阻塞首屏 | 推荐策略 |
|---|---|---|
| 标题/正文 | 是 | 服务端并行获取,失败有状态页 |
| 推荐列表 | 否 | 流式 fallback 或客户端加载 |
| 用户权限 | 通常是 | 请求级上下文,禁止公共缓存 |
| 统计埋点 | 否 | 不进入 SSR 关键路径 |
SSR 数据层的目标不是“服务端拿完所有数据”,而是用最少阻塞数据生成正确首屏,其余内容渐进补齐。
六、序列化、缓存和安全边界
把数据直接拼进 <script> 即使经过 JSON.stringify,仍要处理能提前结束 script 的字符序列和框架序列化协议。应使用框架提供的安全注入能力,只暴露客户端真正需要的字段,不能把数据库记录、内部 token 或服务端密钥整个塞进 HTML。
请求上下文
├─ Cookie/Header/用户身份(仅本请求)
├─ 请求级 memo(同次渲染去重)
├─ 公共数据缓存(明确 key/TTL)
└─ 初始客户端数据(最小化、安全序列化)
一次渲染中 5 个组件都取同一商品,应通过请求级 memo 合并为 1 次;跨请求公共商品可再缓存 60 秒。用户订单绝不能因为 URL 相同进入公共缓存,缓存 key 和数据分类要在数据层强制执行。
超时必须有总 deadline。页面预算 800ms 时,不能让三个接口各自串行等 800ms;关键请求并行并共享剩余预算,非关键模块超时后输出占位,才能控制 TTFB 尾延迟。
七、常见误区与追问
- 误区:SSR 应等待页面所有接口后再返回。 只阻塞首屏正确性依赖的数据,低价值模块可流式或客户端加载。
- 误区:
JSON.stringify后直接拼 script 永远安全。 特殊字符可能打断脚本上下文,应使用框架安全序列化并最小化字段。 - 误区:模块级 Map 可以缓存所有用户请求。 长进程共享模块状态,可能造成跨用户数据泄露。
- 追问:怎样避免客户端重复请求? 把服务端初始数据按框架协议传给客户端查询缓存或状态层,并使用一致 cache key。
- 追问:为什么并行请求不总是更好? 下游容量、依赖关系和连接限制也要考虑,应只并行独立且必要的数据。
- 追问:SSR 接口慢最先影响哪个指标? 通常直接抬高 TTFB,并延后 HTML 解析和后续资源发现。
- 追问:请求级缓存与全局缓存有何区别? 前者只在一次渲染内去重,后者跨用户复用,安全和失效要求完全不同。
八、加强记忆
- 先分类:首屏关键、可流式、可客户端加载三类数据分开。
- 先启动:独立请求并行发出,避免组件树逐层瀑布。
- 请求隔离:身份、Cookie 和用户缓存只存在当前请求上下文。
- 安全传输:初始数据最小化并用框架安全序列化能力注入。
- 多层缓存:请求 memo 去重,公共缓存跨请求,私有数据禁止串用。
- 预算控制:统一 deadline、超时降级和可观测耗时保护 TTFB。