
title: SSR 把后端 CPU 打满后我们换回了 CSR三种渲染模式对后端的真实压力对比description: 从后端工程师视角拆解 SSR、CSR、SSG 的技术原理分析它们对 API 负载、缓存策略和部署架构的不同要求。tags: [SSR, CSR, SSG, 前端渲染, 后端架构, Java]2023 年双 11 前我们的电商详情页从 CSR客户端渲染切到了 SSR服务端渲染。预期是首屏时间从 2.1s 降到 800ms结果上线当天后端 API 集群的 CPU 从 35% 飙到 92%Redis 连接数打满差点把大促搞崩。那次事件让我明白了一个道理SSR 不是前端的技术选型而是后端的容量规划问题。三种渲染模式的本质差异CSRClient-Side Rendering浏览器下载一个空 HTML 壳子然后加载 JSJS 再发 AJAX 请求拿数据最后在浏览器里拼装 DOM。后端只提供静态资源和 API 数据。SSRServer-Side Rendering后端收到请求后先在服务端执行前端代码React/Vue生成完整的 HTML再发给浏览器。浏览器拿到的是 已经渲染好的页面但交互还需要 hydrate激活JS。SSGStatic Site Generation构建时就把所有页面预渲染成静态 HTML请求时直接返回文件。后端压力最小但只适合内容不频繁变化的场景。// 后端视角CSR 和 SSR 的请求处理差异 // CSR 场景后端只返回 JSON GetMapping(/api/product/{id}) public ProductDTO getProduct(PathVariable Long id) { // 查数据库/缓存返回 JSON return productService.getById(id); } // SSR 场景后端要执行前端渲染逻辑 GetMapping(/product/{id}) public String renderProductPage(PathVariable Long id, Model model) { // 1. 查数据和 CSR 一样 ProductDTO product productService.getById(id); // 2. 把数据喂给 SSR 引擎如 Spring Thymeleaf或 Node.js 渲染服务 model.addAttribute(product, product); return product-detail; // Thymeleaf 模板渲染 }CSR 的后端逻辑很简单查数据、序列化 JSON、返回。SSR 的后端逻辑多了模板渲染这一步而这步的 CPU 消耗可能远超你的想象。SSR 为什么打满后端 CPU我们的 SSR 方案是 Node.js 渲染服务 Java API 后端。Node 服务负责执行 React 组件渲染Java 后端提供 GraphQL API。问题出在三个地方每次请求都要重新渲染CSR 的 HTML 可以 CDN 缓存但 SSR 的 HTML 是 千人千面 的推荐商品、用户优惠券、库存状态都不同缓存命中率极低。Node.js 渲染是 CPU 密集型React 的renderToString是纯同步计算一个复杂详情页要执行 200 个组件的 render 方法单次渲染 80-150ms。API 请求放大SSR 渲染时需要的数据CSR 可以分批懒加载先显示骨架屏再加载评论、推荐。但 SSR 要求 一次性拿到所有数据才能出 HTML导致首屏请求的 API 数量是 CSR 的 2-3 倍。// 我们压测时记录的数据SSR vs CSR 的后端 QPS 对比 // 机器8C16G相同业务逻辑 // CSR 场景 // - 静态资源CDN 承载后端不处理 // - API 请求/api/product/{id} 命中 Redis 缓存单机 QPS 约 4500 // - CPU 占用35% // SSR 场景 // - Node 渲染服务单机 QPS 约 180不是 1800是 180 // - Java API因为每次渲染发 8-12 个 API 请求实际打到 Java 的 QPS 180 * 10 1800 // - CPU 占用Node 92%Java 78%4500 vs 180这就是 SSR 和 CSR 在后端吞吐量上的差距。不是 10%不是 50%是25 倍。我们的折中方案SSR 只渲染首屏其余 CSR大促后的复盘会上我们决定不放弃 SSRSEO 和首屏体验确实好但要控制范围只有 SEO 敏感的页面做 SSR商品详情页、类目列表页SSR 只做 Above the Fold 首屏可见区域首屏以下的内容推荐商品、用户评价用 CSR 懒加载SSR 输出加 CDN 缓存虽然千人千面但 未登录用户看到的默认页面 占 60% 流量这部分可以缓存 5 分钟// 后端 SSR 缓存策略Spring Boot Redis Service public class SsrCacheService { Autowired private StringRedisTemplate redis; public String getRenderedHtml(Long productId, String userSegment) { // 缓存 key商品ID 用户分群不是用户ID否则缓存失效 String cacheKey ssr:product: productId : userSegment; String cached redis.opsForValue().get(cacheKey); if (cached ! null) return cached; // 未命中执行 SSR 渲染 String html ssrRenderer.render(productId, userSegment); // 写入缓存TTL 5 分钟 redis.opsForValue().set(cacheKey, html, Duration.ofMinutes(5)); return html; } }userSegment是用户分群标签如 新客、老客、VIP不是userId。这样未登录用户默认走 anonymous 分群缓存命中率提升到 60% 以上。5 分钟 TTL 是因为价格和库存会变化不能缓存太久。这个方案上线后Node 渲染服务的 CPU 从 92% 降到 55%Java API 的 CPU 从 78% 降到 48%。首屏时间从 2.1s纯 CSR降到 1.1sSSR 首屏 CSR 剩余虽然不如纯 SSR 的 800ms但后端扛得住。SSG被遗忘的优等生我们的商品详情页有 200 万个 SKU不可能每次访问都 SSR。但实际上80% 的流量集中在 5% 的热销商品上。这部分商品完全可以用 SSG 预渲染。// SSG 预渲染任务定时把热销商品预渲染成静态 HTML Component public class SsgPreRenderJob { Scheduled(fixedRate 300_000) // 每 5 分钟跑一次 public void preRenderHotProducts() { ListLong hotProductIds productService.getTopProducts(10000); for (Long id : hotProductIds) { String html ssrRenderer.render(id, anonymous); // 写入对象存储如 MinIO/S3Nginx 直接回源 objectStorage.put(ssg/product/ id .html, html); } } }getTopProducts(10000)取前 1 万个热销商品。预渲染的 HTML 放到对象存储前端 CDN 回源时直接拿静态文件后端完全无压力。这 1 万个商品覆盖了 80% 的流量剩下 20% 的长尾商品走 SSR。SSG 的代价是数据延迟预渲染的 HTML 最多延迟 5 分钟。对于价格和库存我们在前端加了一层 数据校验页面加载后发一个轻量 API 请求校验 当前价格是否与 HTML 里的一致不一致就局部刷新。CDN 缓存策略三种模式的边缘节点行为差异很多人以为 上了 CDN 就能扛住流量但 CSR、SSR、SSG 在 CDN 边缘节点上的缓存行为完全不同。CSRCDN 只缓存静态资源JS/CSS/图片HTML 本身几乎不缓存因为每个用户的 HTML 是一样的空壳子。API 请求完全不走 CDN直接打到源站。SSRCDN 可以缓存渲染好的 HTML但缓存 key 怎么设计是个难题。如果按 URL 缓存登录用户和非登录用户看到的一样如果按 Cookie 缓存CDN 命中率暴跌。我们的做法是Nginx 层做边缘渲染缓存只缓存未登录用户的默认页面。// Nginx 配置SSR 输出的边缘缓存 // 只缓存没有登录 Cookie 的请求 location /product/ { proxy_pass http://ssr-render-service; # 如果请求带了 session cookie不走缓存 if ($http_cookie ~* sessionId) { set $no_cache 1; } proxy_cache_bypass $no_cache; proxy_no_cache $no_cache; # 缓存 5 分钟 proxy_cache_valid 200 5m; proxy_cache_key $scheme$request_method$host$request_uri; }proxy_cache_bypass和proxy_no_cache配合$http_cookie判断有sessionId的请求直接透传到 SSR 服务无 Cookie 的请求走 Nginx 缓存。这个配置让我们的 SSR 服务在高峰期少处理了 40% 的请求。SSGCDN 天然适合 SSG因为 HTML 是静态文件。但有一个坑HTML 里内联的 JSON 数据hydration data也会缓存。如果价格是内联在 HTML 里的CDN 缓存 5 分钟意味着用户可能看到 5 分钟前的价格。我们的解决方案是静态 HTML 异步价格填充SSG 的 HTML 里价格区域留空页面加载后 JS 发一个轻量 API 请求获取实时价格。这样 CDN 可以大胆缓存 HTML价格又是实时的。// 价格异步填充的后端接口极轻量只查缓存 GetMapping(/api/product/{id}/price) Cacheable(value product-price, key #id) public PriceDTO getPrice(PathVariable Long id) { // 只查 Redis不查数据库 return priceCache.get(id); }Cacheable(product-price)把价格缓存到 RedisTTL 30 秒。这个接口的平均响应时间 3-5msCDN 不需要缓存它因为价格本身有 30 秒延迟窗口。三种模式对后端的影响对比维度CSRSSRSSG后端 QPS 压力低API 可缓存极高渲染 API极低静态文件首屏时间慢2s快800ms最快CDN 边缘节点数据实时性实时实时延迟5-10 分钟适用场景后台管理、DashboardSEO 敏感 实时性要求高内容型页面、商品详情后端复杂度低高需要渲染服务中需要预渲染任务总结从后端工程师的角度看渲染模式的选择不是 哪个技术新就用哪个而是后端能扛住多少 QPS 的问题。SSR 的收益是用户体验代价是后端的 10-25 倍容量需求。我的建议是-先做 SSG能把 80% 的流量变成静态文件这是性价比最高的优化-SSR 控制范围只渲染首屏以下 CSR 懒加载加用户分群缓存-CSR 保留内部系统、实时性要求不高的场景CSR 是最便宜的选择那次双 11 的惊魂让我记住前端的技术债最后都会变成后端的容量账单。思考题你现在的系统里有 SSR 吗有没有监控过后端渲染服务的 CPU 和 API 调用放大倍数如果让你给一个 100 万 PV/天的商品详情页选渲染方案你会怎么组合 SSR/CSR/SSG各自占多少比例