SSR、SSG、ISR、CSR 到底怎么选?同一个商品详情页我写了 4 遍,跑完才发现我一直搞错了重点

🔑 关键词:SSR,SSG,ISR,Core Web Vitals,Next.js

📖 摘要:同一个商品详情页,我用 CSR / SSG / ISR / SSR 四种方式各写了一遍,在 Slow 4G + 4 倍 CPU 降速下每版跑 5 轮取中位数,把 TTFB、LCP、INP、首屏 JS 体积和服务器成本摊开对比。结论和官方文档讲的不太一样:真正卡住 LCP 的是主图的发现时机,跟渲染模式关系没那么大。

去年 11 月,我被拉去救一个商品详情页。

图片

运营那边说首屏必须压到 1 秒内,产品经理的原话是「用户等 1.2 秒就走了」。我当时想都没想就答了「上 SSR 啊」,因为干这行七八年,脑子里一直刻着一条常识:服务端渲染的首屏一定比客户端渲染快。然后我们花了两周把项目从 Vite 迁到 Next.js 15,改成动态渲染,Lighthouse 移动端上的 LCP 从 3.4s 掉到 3.1s。

快了 0.3 秒。服务器账单从每月 60 多块变成 190 多块。

那天晚上我盯着报告看了很久,第一次认真怀疑一件事:我是不是把 TTFB 和 LCP 这两件事混着记了好几年。所以后面那个月,我把同一个详情页写了四遍。

一、先把测试条件说清楚,不然结论没意义

项目不大:32 个 React 组件,一张主图、一个规格选择器、评论区首屏 10 条、一个猜你喜欢的横滑。数据来自内部商品 API,我实测平均响应 180ms,P95 约 420ms——这个数字后面很关键。

环境:M1 Pro / 16G / macOS 14.5,Node 22.11.0,pnpm 9.15.4。Lighthouse 12.2.1 移动端预设(Slow 4G + 4 倍 CPU 降速),每版跑 5 次取中位数,冷缓存。服务端一侧分别在 Vercel Hobby 和一台 4C8G 阿里云 ECS(Nginx + pm2 cluster 模式)上各跑一轮。

图片

四个版本分别是:

  • 纯 CSR:Vite 6.0.7 + React Router 7,数据在 route loader 里拉
  • SSG:Astro 5.1,output: 'static',构建时预渲染最热的 5000 个 SKU,其余页面 export const prerender = false
  • ISR:Next.js 15.1.4,路由段上写 export const revalidate = 60
  • SSR:同一个 Next.js 项目,改成 export const dynamic = 'force-dynamic'

顺便提一句,Astro 5 把 output: 'hybrid' 删掉了,现在只有 static 和 server 两个值。我一开始照着网上 4.x 的老教程配,build 直接报错,卡了快一个下午。这种细节官方 changelog 里就一行字,但你撞上去就是半天。

二、跑出来的数字

版本 TTFB p75 LCP p75 INP p75 首屏 JS(gzip) 单请求成本
CSR 42ms 3.4s 180ms 186 KB 接近 0
SSG 38ms 1.6s 210ms 92 KB CDN 回源费
ISR 145ms 1.9s 205ms 108 KB 低
SSR 240ms 2.1s 190ms 118 KB 高

先看 TTFB 那一列:CSR 42ms,SSR 240ms,CSR 赢了将近 6 倍。再看 LCP:CSR 3.4s,SSR 2.1s,CSR 输了一半还多。

图片

这两个数同时成立,一点都不矛盾。CSR 的 TTFB 之所以快,是因为服务器返回的就是个几乎空的 HTML 壳,一个字节的业务数据都没有,当然快。而 LCP 要等的东西是:JS 下载 → 解析 → 执行 → hydration → 发 API 请求 → 拿到数据 → 渲染 → 图片解码。链路上每一环都在移动端 4 倍降速下被放大了。SSR 反过来,TTFB 慢是因为服务器要等那个 180ms 的 API,但它把「等」这件事挪到了服务端,浏览器拿到 HTML 的时候内容已经在里面了。

所以 TTFB 快和首屏快,真的不是一回事。我以前做性能优化的时候老盯着 TTFB 看,现在回头看,那段时间有一半的结论是错的。

三、但我发现真正的瓶颈根本不是渲染模式

这个是我做完四版之后最想说的东西。

四版都跑完的那周,我顺手在 SSR 版的 <head> 里加了一行:

<link rel="preload" as="image" href="/img/sku-8842-main.webp" fetchpriority="high">

LCP 从 2.1s 掉到 1.5s。0.6 秒。

图片

而我前面折腾两周换渲染模式,一共才换回来 1.3 秒,还搭上了三倍服务器成本。一行 preload 干掉了将近一半的收益,而且这行代码在 CSR / SSG / ISR / SSR 四个版本上都能用,跟渲染模式一点关系都没有。

原因也不复杂。Chrome 的预加载扫描器(preload scanner)在解析 HTML 的时候会去找主图,但如果主图地址是写在一段 JS 里、或者要等 hydration 之后才动态创建 <img>,扫描器根本看不见它。等你 JS 跑完,LCP 的计时早就开始跑了。把图提前告诉浏览器,等于把「发现资源」这一步从 LCP 的关键路径上挪走了。

所以我的结论是:首屏性能的主战场是 HTML 里资源的发现顺序,不是渲染模式。 渲染模式决定的是「内容什么时候进 HTML」,而 LCP 抱怨的往往是「图片和字体什么时候被发现」。这两件事被很多文章混在一起讲,包括我自己几年前写的那篇。

四、那什么时候选哪个?我现在的实际判断流程

下面这套是我现在接新项目会走的顺序,带具体阈值,你可以直接抄:

  1. 页面数据变化频率低于 1 分钟一次,且能枚举出 URL(商品详情、文章、榜单)→ 一律 SSG + CDN。 别犹豫,SSG 的 TTFB 是这四个里最低的(我这次测到 38ms),成本也最便宜。
  2. URL 太多没法全量预渲染(比如 12 万个 SKU)→ 只预渲染 Top N,剩下的走 ISR。 我这次 N 一开始取的是 5000,astro build 在 CI 上跑了 11 分钟就开始超时了,后来砍到 500,剩下的靠边缘缓存兜底,LCP 只退化了 0.1s。
  3. 数据变化在 10 秒到 10 分钟之间,且 URL 可枚举 → ISR,revalidate 设 60 起。 这是性价比最高的区间。注意配上 Cache-Control: public, s-maxage=60, stale-while-revalidate=600,不然过期那一刻几个热门 SKU 会一起回源,我这边就撞过一次,API 直接被打了 300 多 QPS。
  4. 强个性化(登录态、价格随用户变、库存实时扣减)→ 才上 SSR。 这是 SSR 唯一真正不可替代的场景。单纯为了「首屏快」上 SSR,从我这张表看是亏的。
  5. 纯后台、纯登录后页面 → CSR 就挺好。 后台没有 SEO 需求,用户也不会因为多等 1 秒就跑,反而 CSR 部署简单、服务器几乎不要钱。

图片

有个地方我自己也还在犹豫:INP。表里 CSR 的 INP 是最好的(180ms),因为它的主线程虽然一开始忙,但忙完就没事了;SSR 版 hydration 的时候主线程被占住,用户点规格选择器会有大概 100ms 的延迟感。如果你的页面交互很重(配置器、地图、编辑器),SSR 在 CWV 上其实是拆东墙补西墙,LCP 会好看,INP 会难看。

五、我踩过的几个坑,都挺蠢的

  • ISR 的 revalidate 是路由段级别的,不是全局的。 我一开始写在 app/layout.tsx 里,以为全局生效,结果整棵子树都被拖着一起重验证,日志里全是意外的回源。
  • CSR 版首屏白屏时间长。 第一版我把 fetch 写在 useEffect 里,得等 hydration 完才开始请求。后来挪到 React Router 7 的 loader 里,请求和 JS 并行发出去了,LCP 好了大概 0.4s。
  • Astro 的 client:visible 用过头。 我一开始把规格选择器也设成 client:visible,想着省 JS。结果它在首屏视口内,进入视口那一刻才开始加载组件 JS,用户点第一下是没反应的,体感很差。首屏内的交互组件老老实实 client:load。
  • preload 不是加得越多越好。 我一度把主图、字体、评论区的头像全都 preload 了,LCP 反而涨了 0.2s。原因很简单,preload 是抢带宽的,你抢了太多,真正关键的那一个反而被挤到后面。控制在 2 个以内。

六、还有一个我试了但没上生产的东西

Speculation Rules API。Chrome 109 之后支持,在列表页写一段:

<script type="speculationrules">
{"prerender":[{"source":"document","where":{"href_matches":"/item/*"},"eagerness":"moderate"}]}
</script>

图片

从列表页点进详情页,LCP 能到 0.4s 左右,体感基本上是秒开。我没上的原因是 eagerness 设成 moderate 之后 QPS 还是涨了大概 35%,而我们那个列表页本身流量就大,评估下来不划算。如果你的列表页点击率很高、后端又扛得住,这个真的值得试。

不过 Safari 和 Firefox 现在还不支持,所以它只能当锦上添花,不能当方案。

最后

我现在接新项目的默认答案变成了:能 SSG 就 SSG,SSG 不行就 ISR,SSR 只留给真正需要个性化的页面。 这个结论跟三年前的我说的完全不一样,那时候我觉得不上 SSR 就是不懂性能。

当然也要说清楚反面:如果你做的是 SaaS 后台、需要实时权限判断、或者页面里有大量用户态数据,那 SSR 还是对的,上面那张表完全不适用。以及 SSG 的构建时间是真的会爆炸,页面数过了几万条,CI 那关就是个大坑,别听人说「预渲染一下就完事了」。

真正让我改主意的不是那张表,是那一行 preload。它让我意识到过去几年我在「换个框架重写」上花的时间,可能有一大半花错了地方。

🏷️ 标签: