SSR 与 SSG 的负载对比:高并发场景下服务端压力实测分析
SSR 与 SSG 的负载对比高并发场景下服务端压力实测分析一、同样是页面渲染CPU 消耗可能差 10 倍很多团队把 SSR 当默认选择理由是SEO 友好 首屏快。但很少有人认真测过同样 500 QPS 的页面请求SSR 和 SSG 对服务器的压力差距到底多大。我搭建了一套测试环境Next.js 14 应用同一个电商商品列表页含 API 数据获取、组件渲染分别跑 SSR 和 SSG用 k6 压 500 并发 30 秒。结果SSGP99 延迟 45msCPU 利用率 22%Node.js 进程 RSS 稳定在 180MBSSRP99 延迟 380msCPU 利用率 87%RSS 在 280MB~420MB 间波动差距的核心不是网络 IOAPI 接口是内网直连延迟 5ms而是每个请求都要在服务端重新执行 React 渲染。flowchart TD A[用户请求] -- B{渲染模式} B --|SSR| C[Node.js 服务器] B --|SSG| D[CDN / 静态存储] C -- E[请求数据 API] C -- F[React renderToString] C -- G[模板嵌入数据] C -- H[序列化 dehydration data] E -- F F -- G G -- H H -- I[返回 HTML] D -- J[直接返回预编译 HTML] J -- K[返回 HTML] I -- L[P99: 380ms] K -- M[P99: 45ms]二、SSR 的性能瓶颈链分析瓶颈一renderToString 是同步阻塞的React 的renderToString会阻塞 Node.js 事件循环。高并发下多个请求排队等待渲染事件循环延迟飙升。React 18 的renderToPipeableStream虽是流式但总 CPU 消耗没降——只是把阻塞拆成了多段。瓶颈二每次请求都重建组件树SSG 在构建时渲染一次之后只是读文件。SSR 每次请求都重建整棵组件树包括getServerSideProps中的 API 请求——即使数据在 CDN 缓存中已经存在。瓶颈三Serialization 开销React 需要序列化服务端渲染状态为__NEXT_DATA__脚本注入到 HTML 中hydration 阶段再反序列化。这个 JSON 随着组件树复杂度线性增长。商品列表页的__NEXT_DATA__经常达到 50KB。三、压测代码与对比实验// k6 压测脚本ssr-vs-ssg-benchmark.js import http from k6/http; import { check, sleep } from k6; import { Trend, Rate } from k6/metrics; const ssrLatency new Trend(ssr_latency); const ssgLatency new Trend(ssg_latency); const errorRate new Rate(errors); export const options { // 分阶段加压先预热 30s 低负载再梯度加压 stages: [ { duration: 30s, target: 50 }, { duration: 60s, target: 200 }, { duration: 60s, target: 500 }, { duration: 30s, target: 0 }, // 冷却 ], thresholds: { // P95 延迟不能超过 1s否则判定不合格 ssr_latency: [p(95)1000], }, }; const SSR_URL http://localhost:3000/products; const SSG_URL http://localhost:3000/products-ssg; // 模拟真实用户的商品筛选请求 function buildProductQuery() { const categories [electronics, books, clothing]; const page Math.floor(Math.random() * 10) 1; const category categories[Math.floor(Math.random() * categories.length)]; return ?category${category}page${page}sortprice_asc; } export default function () { // SSR 请求 const ssrStart Date.now(); const ssrRes http.get(${SSR_URL}${buildProductQuery()}, { headers: { Accept-Encoding: gzip }, timeout: 10s, }); ssrLatency.add(Date.now() - ssrStart); check(ssrRes, { SSR status 200: (r) r.status 200, SSR contains products: (r) r.body.includes(product-card), }) || errorRate.add(1); sleep(0.5); // SSG 请求同一用户切换视图 const ssgStart Date.now(); const ssgRes http.get(${SSG_URL}${buildProductQuery()}, { headers: { Accept-Encoding: gzip }, }); ssgLatency.add(Date.now() - ssgStart); check(ssgRes, { SSG status 200: (r) r.status 200, SSG contains products: (r) r.body.includes(product-card), }) || errorRate.add(1); sleep(1); } export function handleSummary(data) { return { benchmark-summary.json: JSON.stringify(data), stdout: Benchmark Results SSR P50: ${data.metrics.ssr_latency?.values?.med?.toFixed(0)}ms SSR P99: ${data.metrics.ssr_latency?.values?.[p(99)]?.toFixed(0)}ms SSG P50: ${data.metrics.ssg_latency?.values?.med?.toFixed(0)}ms SSG P99: ${data.metrics.ssg_latency?.values?.[p(99)]?.toFixed(0)}ms Error Rate: ${(data.metrics.errors?.values?.rate * 100).toFixed(2)}% , }; }四、选型决策与混合策略什么时候用 SSG内容更新频率 ≤ 1 次/小时博客、文档站、企业官网QPS 200 且服务器成本敏感SEO 要求高但内容相对静态什么时候用 SSR需要用户个性化数据登录后展示不同内容数据实时性要求高秒级更新QPS 100 且愿意为 SEO 支付服务端算力ISR两者的中间地带Next.js 的 ISRIncremental Static Regeneration在 SSG 的基础上加了按时间或按需重新生成的机制。首次请求返回缓存 HTML后台异步重新渲染下次请求返回新版。混合策略列表页 → ISRrevalidate: 60s详情页 → SSG构建时生成购物车 → SSR用户强相关结账页 → CSR纯动态交互一个常被忽视的细节CDN 缓存头SSR 返回的 HTML 默认不带Cache-ControlCDN 不会缓存。这意味着每一个请求都穿透到源站。正确的做法是根据页面特性设置 CDN 缓存策略——即使只缓存 5 秒在 500 QPS 场景下也能减少 2500 次源站渲染。五、总结SSR 不是为了性能而是为了实时个性化 SEO。如果内容相对静态SSG 的性能和成本优势是碾压级的。在生产环境中最佳实践不是二选一而是按页面特征做混合策略——同一条产品线的列表页用 ISR、详情页用 SSG、管理端用 CSR各取所需。