3136 字
约 10 分钟
17
S14-A04 · 前端交付策略:SSR、静态生成与客户端渲染的取舍

渲染模式的选择常被简化成"要不要 SEO",这是把架构决策降级成单一指标。真正的差别在三个位置:HTML 何时生成、JS 在谁的机器上执行、数据在哪一侧取。这三件事一定下来,就同时决定了首屏指标、服务端成本、缓存可行性与发布流程。下面按"差别 → 代价 → 缓存 → 迁移"给判断结构;框架细节按 Next.js 14/15、Astro 4/5 通用语义,开关按所部署版本核对。


1. 三类模式的本质差别

模式 HTML 生成时机 执行 JS 的位置 数据在哪取
SSR 每次请求(或缓存命中时) 服务端渲染 + 客户端水合 请求期,服务端
静态生成 构建期(或增量再生成) 客户端水合 构建期
客户端渲染 浏览器内由 JS 生成 全部在客户端 请求期,客户端

差别不在"有没有 HTML",而在首字节里有什么:SSR 与静态生成含内容,客户端渲染通常只是空壳加脚本。


2. 首屏指标与模式的对应

指标 SSR 静态生成 客户端渲染 关键影响
TTFB 受服务端渲染耗时影响 最低(可直接 CDN 命中) 低(壳很快) 服务端逻辑与缓存命中率
FCP 快 最快 慢,等 JS 下载执行 首包大小
LCP 取决于数据就绪时间 好 常被 JS 阻塞 数据取用位置
TTI / INP 受水合成本影响 受水合成本影响 受整包执行影响 水合范围与拆包
常见误判:TTFB 好看不代表体验好。
静态页 TTFB 很低,但若水合包过大,INP 会很差;
反过来 SSR 页 TTFB 高,却可能因为内容已就位而 LCP 更早。

指标要成对看:TTFB 配 LCP,FCP 配 INP。单看一项,容易选出在另一项上崩掉的方案。


3. SSR 的代价

代价 机制 缓解手段
服务端负载 每次请求都要执行渲染与数据请求 页面级缓存、增量再生
缓存难度 内容随用户/参数变化,命中率低 按 URL + 少量维度做键
状态序列化 服务端数据要注入客户端,体积与安全都要管 只传必要字段,避免整对象透传
部署复杂度 需要常驻进程、健康检查、扩容 边缘/无服务运行时或容器化
// 页面级缓存响应头:给 CDN 与浏览器不同的 TTL
res.setHeader('Cache-Control', 's-maxage=60, stale-while-revalidate=300');

stale-while-revalidate 是 SSR 场景性价比最高的一条:过期后先返回旧内容,后台再生成,把渲染耗时从用户等待里挪走。代价是内容有一个再生成窗口的滞后,这个窗口要写进业务预期。

适合 SSR 的页面:个性化强、实时性要求高、首屏重要的页面(用户中心、商品详情、搜索结果)。


4. 静态生成的适用边界

条件 适合静态生成 不适合
内容更新频率 分钟级以上 秒级实时
页面总量 构建时间可接受 数十万页全量构建
个性化 无或可按 URL 参数切分 依赖登录态
数据源稳定性 构建期可取到 只能请求期取
// 增量再生成:构建期生成 + 到期后台重建(按版本核对 API 名)
export const revalidate = 3600;
export async function generateStaticParams() {
  const posts = await listPublishedSlugs();
  return posts.map((s) => ({ slug: s }));
}

静态生成的隐性成本是构建时间随页面数线性增长。上千页后全量构建从分钟级走向十几分钟,发布节奏被构建时长锁死。此时要的是增量构建 + 按需失效,而不是加构建机器。


5. 客户端渲染的合理性

后台系统、强交互页面、登录后才有意义的界面,客户端渲染是正解:数据本来就在客户端上下文里,服务端渲染只是多一次往返。

判断清单(满足多数则选客户端渲染):
1. 页面不在索引范围内
2. 首屏之外的操作占比高(表单、表格、编辑器)
3. 数据依赖客户端状态(token、本地缓存)
4. 无 SEO 与分享卡片需求

"SEO 与首屏"的真实影响面:对外内容页大,对内系统近乎为零。为内部后台引入 SSR,换来常驻服务、部署复杂度与一类新故障(渲染进程挂掉整站白屏),收益接近零。


6. 混合渲染的成本收益

方案 收益 成本
岛屿架构(部分水合) 交互组件才加载 JS,首包显著变小 组件边界要重新划分,跨岛状态传递受限
按路由分流 对外页静态、后台页 CSR 两套构建与部署路径并存
流式渲染 + Suspense 首字节更早,慢区块后到 需要服务端支持分块传输,错误处理更复杂
---
// 岛屿:只有这个组件在客户端水合
---
<SearchBox client:visible />
<StaticArticle />
// 流式:先送出外壳,慢区块后补
const shell = <Layout><Header /></Layout>;
return new Response(stream, { headers: { 'Content-Type': 'text/html' } });

混合渲染的前提是**"静态大头 + 少量交互"的比例足够高**。每个区块都要交互时,岛屿划分只是把复杂度摊到组件边界,收益接近零。


7. 缓存四层与失效传播

层 缓存什么 失效方式 风险
CDN 完整 HTML/静态资源 TTL、按路径清除 清除延迟、命中率低
服务端 渲染结果、数据片段 TTL、主动失效 键设计错导致串数据
数据层 查询结果 写后失效 失效与写不在一个事务里
客户端 内存/本地存储 版本号、TTL 旧版本 UI 与新接口不兼容
# 静态资源长缓存 + 指纹;HTML 短缓存
location ~* \.(js|css|woff2)$ {
  add_header Cache-Control "public, max-age=31536000, immutable";
}
location / {
  add_header Cache-Control "public, max-age=0, s-maxage=60";
}

失效传播必须从内到外:数据层 → 服务端 → CDN。反过来做,外层会在内层更新后重新缓存住旧内容,形成"清了又回来"的循环。


8. 数据获取位置与一致性风险

位置 延迟 一致性风险 适用
构建期 最低 内容滞后一个构建周期 低频变更内容
请求期(服务端) 中 与源数据近实时,但受缓存影响 详情页、列表首页
客户端 高(多一次往返) 可能出现"页面已渲染、数据未到" 个性化、实时交互
// 同一页面里的三种取数要显式标注,避免"看起来一致"
const cfg   = await fetchAtBuild('/api/site-config');   // 构建期
const list  = await fetchPerRequest('/api/posts');      // 请求期
const mine  = await fetchOnClient('/api/me');           // 客户端

一致性风险集中在同一屏混用三种取数:价格来自构建期、库存来自请求期,不同步就是"页面自相矛盾"。这不是渲染模式的错,是取数位置没按数据敏感度分层。


9. 多页与单页的取舍

维度 多页(路由级页面) 单页(长驻应用)
路由 服务端路由,天然可分享 前端路由,需额外处理深链
状态 页面级,刷新即重置 全局常驻,易泄漏
分享链接 直接可用 依赖路由实现质量
内存 随导航释放 长期增长
首屏 每页独立,可控 首包偏大
选择口径:内容型站点走多页,工具型应用走单页,
混合站点按"入口页多页 + 应用区内单页"切分

单页的内存问题在长时间使用后显现:路由切换不释放的监听、缓存、闭包会持续累积。这是"应用不重启"这个前提的必然代价,不是框架缺陷。


10. 构建产物、可观测与迁移

# 产物核对:指纹、首包体积、预加载清单
ls dist/_astro/*.js | head
du -sh dist/client dist/server
观测口径 优点 局限
实验室指标(Lighthouse) 可复现、可比对 与真实网络/设备差距大
真实用户指标(RUM) 反映真实分布 需要采样,长尾要足够样本
服务端渲染耗时 直接反映 SSR 成本 不含水合
// RUM 采样:低采样率下按页面类型分层,避免长尾页被平均掉
new PerformanceObserver((l) => {
  for (const e of l.getEntries()) report('lcp', e.startTime, { route });
}).observe({ type: 'largest-contentful-paint', buffered: true });

迁移按风险递增:先静态化首屏 → 按路由分流(对外静态、后台 CSR) → 统一缓存与失效策略。一次全站切 SSR 的问题不在可行性,而在把首屏、缓存、部署三件事同时变成未知数,出问题无法定位层级。

迁移阶段 验证口径
静态化首屏 LCP 与 TTFB 对比基线
按路由分流 各路由的错误率与渲染耗时
统一缓存 CDN 命中率与失效延迟

11. 排错:五类交付异常

现象 原因 处理
首屏白屏时间长 首包过大、水合阻塞 拆包、减少水合范围
内容与接口不一致 取数位置混用、缓存键缺维度 显式分层取数,补缓存键
清缓存后仍旧内容 失效顺序从外到内 改为数据 → 服务端 → CDN
部署后部分用户报错 静态资源与新接口版本不匹配 资源指纹 + 接口版本兼容窗口
SSR 页面偶尔 500 渲染期取数失败未兜底 渲染降级为骨架 + 客户端补数据

最后一条最易忽略:SSR 把一次局部数据失败升级成整页 500。渲染路径必须有降级,否则可用性低于纯客户端方案。


12. 边界:这些情况不值得上 SSR

场景 判断 理由
纯内部系统 客户端渲染 无 SEO 需求,SSR 只增加部署面
极低频访问页面 静态生成或按需生成 常驻渲染成本换不到收益
内容几乎不变 纯静态 每次请求渲染是浪费
强实时个性化 + 高并发 壳静态 + 客户端取数 服务端渲染成本随并发线性上升
团队无服务端运维能力 静态优先 SSR 引入常驻进程与一类新故障

13. 自测

  1. 三类渲染模式的本质差别,用哪三个位置可以一次说清?
  2. 为什么 TTFB 低不代表体验好?该和哪个指标配对看?
  3. stale-while-revalidate 把哪部分耗时挪走了,代价是什么?
  4. 静态生成的隐性成本为什么随页面数增长?
  5. 缓存失效为什么要从内到外,反过来会出现什么现象?
  6. 迁移到 SSR 时,为什么建议分三段推进而不是一次切换?
S14-A04 · 前端交付策略:SSR、静态生成与客户端渲染的取舍
https://www.clxhxhhr.top/posts/6074/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。