做 Web 性能优化时,SSR(Server-side rendering,服务端渲染)和静态渲染(常见是SSG/ Prerendering)经常被放在一起对比。很多团队会下意识觉得:只要把页面丢到服务端生成HTML,用户就能更快看到内容,搜索引擎也更开心。现实更微妙:SSR的“动态性”会带来明显的算力成本,而且如果实现方式不够讲究,还可能把TTFB拉长,甚至让客户端Hydration变成新的性能瓶颈,最后TBT、INP反而更难看。(web.dev)
下面从浏览器渲染链路出发,把SSR与静态渲染的差异、代价、优化抓手讲清楚,并配一些“真实世界”场景,帮你在工程里做决策。
从浏览器视角看:用户等待的时间,究竟花在哪
当用户访问一个 URL,浏览器大致会经历几件事:
- 网络阶段:发起请求,等待服务器返回响应的第一个字节,也就是
TTFB(Time to First Byte)。如果服务器能更早“冲刷”响应(例如先把HTTP头或<head>发出去),TTFB往往会更好看。(web.dev) - 解析与渲染阶段:浏览器解析
HTML,构建DOM,再结合CSS形成渲染结果。 - 交互阶段:用户能不能“点得动”,取决于主线程是否被长任务占住。实验室里常用
TBT(Total Blocking Time)衡量主线程在页面加载期间被阻塞的总时长。(web.dev) - 真实交互体验:线上更关注
INP(Interaction to Next Paint),它观察用户访问生命周期内的点击、键盘、触摸等交互延迟,最终取最差(并忽略部分离群值)作为页面响应性指标。(web.dev)
把这条链路记在脑子里,你会发现:SSR让“看到内容”变早,并不自动意味着“交互就更快”。如果Hydration(把服务端生成的静态HTML变成可交互应用的过程)很重,用户看到内容后依然可能“点了没反应”。Hydration的设计本身就是一种折中:服务端先给可见内容,客户端再接管并补齐交互能力。(web.dev)
静态渲染在做什么:把昂贵的工作前置
静态渲染的核心思路很朴素:把页面HTML提前生成,要么在构建期生成,要么在后台按需再生成并缓存起来;用户请求来了就直接发缓存结果。
以Next.js为例,官方把它描述为:静态渲染的路由在构建时或后台再验证后生成,生成结果会进入缓存并在后续请求复用。(Next.js)
静态渲染的优势通常体现在:
TTFB天然更短:因为服务器不需要每次请求都“跑一遍渲染”。- 更容易吃满
CDN缓存:边缘节点就能回源更少。 - 成本更可控:算力高峰时不会因为每个请求都做渲染而爆炸。
代价也很明确:数据可能变陈旧。当然,现代框架提供了再验证、增量生成等机制,试图在“快”和“新鲜”之间找平衡。(Vercel)
SSR在做什么:每个请求都“现场出菜”
SSR的定义同样直接:每个 URL 请求到来时,服务端动态生成HTML,再把它发回浏览器。(web.dev)
它的优势集中在一个词:“活”。
- 你可以在渲染时读取更实时的数据源。
- 你可以应对更复杂、更细分的请求形态。
- 你可以做个性化:例如按用户、按地区、按登录态输出不同内容。这类需求用纯静态渲染往往很别扭。(web.dev)
但这份“活”,会把成本和复杂度带进来。
为什么SSR不是万能解:算力、TTFB、重复工作,三座大山
1) 动态渲染的算力开销是真金白银
SSR每次请求都要跑渲染逻辑,遇到流量峰值,渲染开销会线性放大。相较之下,静态渲染更像“批量生产 + 缓存分发”,天然更省。(web.dev)
2) 不会“早冲刷”的SSR反而会拖慢TTFB
很多SSR方案会等整棵组件树渲染完才把响应发出去,于是用户拿到第一个字节的时间被拖长。浏览器关于TTFB的讨论里也明确提到:部分服务器允许提前冲刷响应(例如只冲刷头部或<head>),这和Early Hints一样,都能让浏览器更早开始工作。(web.dev)
3)Hydration往往意味着“同一个应用做两遍”
很多基于React的典型SSR路径是:
- 服务端渲染:输出
HTML - 客户端再渲染一次:把事件监听、状态等挂回去(
hydrateRoot)
React文档对hydrateRoot的描述很直白:它用于把之前由react-dom/server生成过HTML的DOM节点“显示”成可交互的React应用。(React)
这就带来一个现实问题:更快看到内容,不等于更少的客户端工作量。如果客户端还要下载大包、执行大量JS、做重Hydration,主线程会被长任务压住,实验室的TBT会升高,线上INP也更容易变差。(web.dev)
React里的关键差异:renderToString()与流式SSR
在React生态里,很多人对SSR的第一印象来自renderToString():服务端把组件树同步渲染成一个完整字符串,再send给浏览器。(React)
它的问题在于:同步、串行。当组件树很大、数据依赖多、模板复杂时,服务端需要等全部渲染完成才开始返回。
React新的服务端DOM接口提供了“流式渲染”能力。例如renderToPipeableStream可以把React树渲染到Node.js的流里,从而让“壳子”更早到达浏览器,后续内容边生成边发送。(React)
你可以把它理解成餐厅出菜逻辑:
renderToString():等所有菜都做好再一次性上桌。- 流式
SSR:先把开胃菜和餐具端上来,主菜边做边上。
这类能力的价值,核心就是缩短用户“开始看到东西”的时间窗口,同时尽可能减少因为等待整棵树完成而拉高的TTFB。(React)
静态渲染真的“过时”了吗:看三个真实世界场景就懂
场景 A:营销落地页与文档站
特点是内容更新频率低、访问量可能很高、首屏稳定,几乎不需要按用户差异化输出。
这类页面用静态渲染,配合CDN全缓存,往往能拿到非常漂亮的TTFB,服务器成本也最低。再加上内容可直接以HTML形式被解析,搜索引擎抓取也更顺畅。
工程上常见做法是:正文静态化,少量动态区域(比如“最新公告”或“在线客服”)用客户端请求补齐即可。Next.js也明确提到:你可以用静态生成,再通过客户端数据获取填充部分区域。(Next.js)
场景 B:电商商品详情页与价格库存
电商看起来“很动态”,但并不意味着必须全量SSR。
一个很常见的折中是:
- 商品基础信息(标题、图、详情、参数)静态生成并缓存
- 库存、价格、优惠券等高频变化部分走客户端或边缘接口获取
- 定期再验证或增量生成,保证主要内容不至于过旧
这样做的收益是:大多数用户请求命中缓存,TTFB和服务器成本都更好;动态部分也能保持足够新鲜。
如果你把所有内容都塞进SSR,峰值流量时服务端渲染会变成巨大成本,而且你仍然要面对客户端Hydration的开销,未必比静态方案更“快点得动”。(web.dev)
场景 C:强个性化的首页或工作台
这类页面的关键不是“内容能不能提前生成”,而是“不同用户看到的东西差异很大”,例如:
- 登录后展示用户专属模块、待办、权限相关入口
- 不同地区、不同套餐看到不同价格与权益
- 需要按
Cookie、按用户画像输出
这里SSR的价值就非常明确:服务端可以在渲染期拿到更完整的请求信息,并输出更贴合用户的HTML。同时,你可以用HTML caching做“分层缓存”:按用户段(segment)缓存,而不是按每个具体用户缓存,把成本压下去。(web.dev)
把SSR做对:关键不是“用不用”,而是“怎么省一次渲染”
从工程经验看,SSR的坑大多不在“能不能渲染”,而在“能不能在高并发下稳定且便宜地渲染”。
下面这些抓手很实用。
1) 让响应更早开始:流式渲染 + 早冲刷
如果你用React做SSR,优先考虑流式方案,让壳子更快到达浏览器。renderToPipeableStream的定位就是为Node.js流式输出服务端HTML。(React)
示例(用单引号,避免"):
importexpressfrom'express'importReactfrom'react'import{renderToPipeableStream}from'react-dom/server'importAppfrom'./App.js'constapp=express()app.get('*',(req,res)=>{res.setHeader('Content-Type','text/html; charset=utf-8')const{pipe,abort}=renderToPipeableStream(<App url={req.url}/>,{onShellReady(){res.statusCode=200pipe(res)},onError(err){console.error(err)}})// 防止极端情况下渲染挂死占资源setTimeout(()=>abort(),10000)})app.listen(3000)这段代码的核心价值是:onShellReady时就开始pipe,让浏览器尽早拿到可解析的HTML,把“等待服务端做完全部工作”改成“边做边发”。(React)
2) 做HTML caching,把“动态渲染”变成“动态生成 + 多次复用”
SSR最大的问题是每次请求都渲染。解决它的关键就是缓存。
你可以做的缓存层次包括:
- 全页
HTML缓存:最直接,命中就不渲染。 - 组件级缓存:例如导航栏、页脚、推荐模块,很多时候在短时间内并不变。
- 数据缓存:对同一份数据的重复拉取是另一个隐藏成本。
这也是为什么很多框架会把“渲染策略”与“缓存策略”绑在一起讲。Next.js文档就把渲染策略视为缓存可用性的基础:静态渲染结果可复用,动态渲染则更依赖你如何设计缓存。(Next.js)
3) 控制Hydration的重量:别让主线程被自己打爆
TBT的定义强调了主线程在FCP后被长任务阻塞的总时长;任务超过50 ms的部分会计入阻塞。(web.dev)INP则直接度量交互延迟,最终取最慢交互作为结果。(web.dev)
这意味着:就算SSR让内容更早出现,只要Hydration把主线程占满,用户一样会觉得卡。
实操里常用的减负方法包括:
- 减少首屏
JS体积:拆包、懒加载,把非关键交互推迟。 - 降低一次性
Hydration范围:让关键区域先可交互,非关键区域晚一点再接管。 - 减少重复状态注入:很多
SSR会把初始状态内联进页面,客户端又再拉一遍或再计算一遍,导致“数据发了两次”。这类设计要尽量避免。(web.dev)
你可以把目标理解为:把SSR的收益(更早可见)真正转化成“更早可用”,而不是停留在“更早看到一个不能点的页面”。
一张决策清单:用更少争论换更快落地
下面这套判断在团队里很管用,你可以直接拿去当评审模板。
更偏向静态渲染的信号
- 页面内容对所有用户基本一致
- 内容更新频率不高,允许分钟级甚至小时级再验证
- 流量大、成本敏感,希望尽量命中
CDN - 交互不重,或交互可以局部客户端化
对应例子:品牌官网、活动页、文档站、帮助中心、招聘页、博客正文。
更偏向SSR的信号
- 强个性化:登录态、权限、用户画像驱动的差异明显
- 数据必须请求时最新,且无法接受再验证延迟
- 需要在服务端做复杂聚合,客户端拿不到足够上下文
对应例子:工作台、订单中心、B2B 报价页、带权限的管理后台首页。
混合方案往往是性价比最高的现实解
大多数大型站点最后都会走向混合:
- 页面骨架静态化(快、便宜、可缓存)
- 局部动态区域按需获取或边缘渲染(新鲜、个性化)
SSR只用于那些“静态化会产生明显业务损失”的页面
这类思路和Hydration的折中精神是一致的:把渲染工作拆开,让“该快的快、该活的活”。(web.dev)
收束:真正的结论不是二选一,而是把代价放到正确的位置
SSR的价值很真实:它能在请求时输出更“活”的内容,尤其适合个性化与强实时场景。(web.dev)
它的代价也很真实:动态渲染的算力开销、可能变长的TTFB、以及客户端Hydration带来的主线程压力,会直接反映到TBT与INP上。(web.dev)
工程上更可靠的路线是:
- 能静态化的部分尽量静态化,让缓存吃满
- 必须动态的部分再动态,并用
HTML caching、数据缓存把渲染次数压下去 - 能流式就流式,让响应更早开始
- 把
Hydration当成性能预算来管理,而不是默认全量接管
当你用这套视角回看SSR vs 静态渲染,争论会少很多:因为你不是在选口号,而是在选一条“可测量、可压缩成本、可控风险”的渲染链路。