SSR性能优化深度解析
SSR 三大性能瓶颈(TTFB 长、CPU 高、水合慢)的解法:多级缓存、流式渲染、部分水合。
一句话概括
SSR 解决了首屏白屏和 SEO,却引入了新问题:服务端每次请求都要跑一遍 JS 渲染(TTFB 变长、CPU 飙高),客户端还要整页重新绑定事件(Hydration 慢、可交互时间推迟)。所以 SSR 性能优化就三条主线——缓存(内存→Redis→CDN 多级命中)、流式渲染(React 18 的 renderToPipeableStream 边渲染边发送)、部分水合(只激活用户会交互的区域)。这是 SSR 项目从”能跑”到”能扛住生产流量”的分水岭。
核心知识点
1. 多级缓存:把”每次现算”变成”命中即回”
缓存是 SSR 优化收益最大的一招。核心是分级 + 按请求特征定缓存键:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 内存缓存(进程内,最快,适合热点)
const cache = new Map();
function memGet(key) {
const e = cache.get(key);
if (e && Date.now() - e.t < 60_000) return e.v; // TTL 60s
return null;
}
// 缓存键:登录用户不缓存 / 匿名用户按 URL 缓存
function buildKey(req) {
const authed = !!req.cookies?.session;
return authed ? `user:${req.userId}:${req.url}` : `anon:${req.url}`;
}
// 响应头控制 CDN:浏览器不缓存,CDN 缓存 60s,过期后 stale-while-revalidate 后台更新
function setCacheHeaders(res) {
res.setHeader('Cache-Control',
'public, max-age=0, s-maxage=60, stale-while-revalidate=600');
}
关键点:登录/个性化页面绝不能无脑缓存,否则 A 用户看到 B 用户的页面。Redis 负责跨进程/跨机器共享(分布式多实例时内存缓存各自为政),CDN 负责边缘就近响应。三级加起来能把 P90 从 500ms 打到 10ms。
2. 流式渲染:不等数据全齐,渲染多少发多少
传统 SSR 要等所有 API 返回才 renderToString 一次性输出,慢接口卡住整页。流式渲染用 renderToPipeableStream,骨架先出,慢数据后到:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { renderToPipeableStream } from 'react-dom/server';
app.get('/', (req, res) => {
const { pipe } = renderToPipeableStream(<Page />, {
onShellReady() {
res.setHeader('Content-Type', 'text/html');
pipe(res); // 页面骨架(shell)就绪立即开始流式发送
},
onError(err) { res.status(500).end('error'); }
});
});
// Page 内部:慢组件用 Suspense 包裹,先渲染 fallback 骨架屏
function Page() {
return (
<>
<Header /> {/* 立即渲染 */}
<Suspense fallback={<Skeleton />}>
<ProductDetail /> {/* 数据就绪后流式补上 */}
</Suspense>
</>
);
}
收益直接体现在 TTFB 大幅下降——用户不用等最慢的那个接口。数据是”边渲染边落 HTML 边发”的 chunked 响应,而不是一个憋大招的大字符串。
3. 部分水合:只激活会交互的区域
整页 Hydration 的浪费在于:页脚、版权文案、静态图文这些永远没人点的节点也白绑一遍事件。部分水合按需激活:
1
2
3
4
5
6
7
8
9
10
11
12
// 只对打了 data-hydrate 标记、且进入视口的区块激活
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const name = entry.target.dataset.hydrate;
hydrateComponent(entry.target, name); // 只水合这一小块
observer.unobserve(entry.target);
}
});
}, { rootMargin: '200px' });
document.querySelectorAll('[data-hydrate]').forEach((el) => observer.observe(el));
进阶是渐进式水合(Progressive):视口内交互组件最高优先级立即水合,视口外组件用 requestIdleCallback 空闲时再水合。Astro 的岛屿架构、Qwik 的”零 Hydration”都是这个方向的极端化——能静态就不上 JS。
4. 降低服务端 CPU 开销
SSR 把原本分摊到用户浏览器的渲染压力全集中到了服务器,所以还要在计算侧省:
1
2
3
4
5
6
7
8
9
10
11
12
// 组件级缓存:计算密集但内容稳定的组件,缓存其渲染结果
const memoized = React.memo(function Table({ data }) {
const rows = useMemo(() => data.map(expensiveTransform), [data]);
return <table>{rows.map(...)}</table>;
});
// 服务端避免在渲染路径做重活:
// ❌ 渲染函数里 await 一个 800ms 的数据库全表扫描
// ✅ 数据在进入渲染前就并行取好:Promise.all 而不是 await 串行
const [user, posts, comments] = await Promise.all([
fetchUser(id), fetchPosts(id), fetchComments(id)
]);
Promise.all 并行取数 vs 串行 await,在三个 300ms 接口下能省下 600ms——这是最容易被忽视却最直接的服务端提速点。
5. 关键指标与目标值
| 指标 | 含义 | 目标 |
|---|---|---|
| TTFB | 收到第一个字节的时间 | < 200ms |
| LCP | 最大内容渲染完成 | < 2.5s |
| TTI | 可交互时间 | < 3.5s |
| TBT | 主线程总阻塞时间 | < 100ms |
SSR 的陷阱是:FCP 很好看,但 TTI(可交互)可能比 CSR 还晚——因为浏览器要先下载大 bundle、再整页 Hydration。所以优化的终点不是”首屏快”,而是”首屏快 + 可交互也快”。
其实你每天都在用
- 商品详情页秒开:标题价格主图是 CDN 缓存命中直出的 HTML,你在全国任何地方点开都近,因为边缘节点离你最近
- 刷微博/资讯流:内容流是骨架屏先出来,正文和图片流式加载,你感觉”先看到框架再填内容”就是流式渲染
- 列表页”加载中”骨架:慢接口被 Suspense 兜底,先渲染灰色占位块,数据到了自动替换,页面不白屏
- 页面底部评论区:你不往下滚,评论区 JS 根本不会激活,这就是懒水合在帮你省流量和电量
- 网页点分享”加载不出来”:往往是服务端渲染超时,说明缓存没命中、接口串行阻塞了渲染主流程
常见误解(FAQ)
❌ 误区一:”SSR 一定比 CSR 快”
SSR 快的是首屏内容展示(FCP),但可交互时间(TTI)往往更慢——浏览器要等 JS bundle 下载完再做整页 Hydration,这段时间页面看着能用实际点不动。所以 SSR 不是无条件更优,内容型/SEO 敏感场景才划算,重交互后台系统可能 CSR 更好。
❌ 误区二:”缓存设个 max-age 就完事了”
缓存最难的不是”存”,是失效。文章更新后用户还看到旧内容、登录用户看到别人页面,都是缓存键和失效策略没设计好。正确做法是按请求特征(登录态、个性化)区分缓存键,页面变更时主动 invalidate 相关 key,而不是靠”过期时间到了自然更新”。
❌ 误区三:”流式渲染就是服务端不阻塞了,数据随便串行取”
流式渲染只解决”发送”阶段的等待,解决不了”取数”阶段的等待。如果数据源还是 await A; await B; await C 串行,服务端 CPU 依然被憋着,TTFB 照样难看。流式 + 并行取数(Promise.all)要配合使用。
❌ 误区四:”部分水合后不用管那些没水合的区域”
没水合的区块是纯静态 HTML,用户点了没反应,如果不做降级体验会很怪(比如按钮点了毫无反馈)。部分水合的前提是:要么该区块真的纯展示、要么留了明确的”加载中/点击后激活”兜底,不能把交互功能静默阉割掉。
一句话总结
SSR 优化的核心就一句话:让服务器少算、少传、传得早,让客户端少激活、晚激活、只激活该激活的——缓存降计算、流式抢时间、部分水合省交互,三者叠加才是生产级的 SSR。