2023 年,我用 Next.js 做了一个内容站——技术博客加一套产品文档。开发的时候我没怎么动脑子:页面要数据,我就写 getServerSideProps;它最"万能"——每次请求都在服务器现拉最新数据,我图省事,索性【全站每一个页面】都用它。本地开发跑得飞快,上线初期流量小,一切风平浪静。直到有一天,市场部做了一波推广,流量在一小时内翻了几十倍。我盯着监控,脸都绿了:页面的 TTFB(首字节时间)从平时的 200ms,一路飙到 4 秒;服务器 CPU 直接焊死在 100%;紧接着,大面积 502。用户打开一篇文章,要白屏等四五秒,很多人直接走了。我第一反应是"服务器不够,加机器",运维紧急扩容,缓解了一点,可成本肉眼可见地往上窜。我心里堵得慌——这不对劲。冷静下来我去翻日志,翻到一半我突然僵住了:被打爆的请求里,绝大多数,是【文章详情页】和【文档页】。而这些页面的内容,是【几天甚至几周才改一次】的。可我的代码,却让服务器为【每一个访客、每一次刷新】,都把同一篇文章,从数据库查一遍、用 React 重新渲染一遍。我让服务器,日复一日地、千百万次地,重复生产着【一模一样的东西】。那一刻我才明白:我的服务器不是被流量打垮的,是被我自己写的、海量的【无谓重复劳动】打垮的。这件事逼着我把网页到底是"在哪、在什么时候"被渲染出来的、CSR / SSR / SSG / ISR 这四种策略各自是什么、它们的取舍在哪、以及真实项目里到底该怎么按页面选,彻底理清了一遍。本文是这份梳理的完整复盘。
问题背景:一个"全站 SSR"被流量打垮的内容站
环境:Next.js 做的内容站(技术博客 + 产品文档)
我的做法:图省事,全站每个页面都用 getServerSideProps(SSR)
事故经过:
- ★ 市场推广,流量一小时内翻几十倍
- ★ 页面 TTFB 从 200ms 飙到 4 秒,服务器 CPU 焊死 100%
- ★★ 大面积 502,用户白屏等四五秒,大量流失
★ 我的第一反应:服务器不够 -> 加机器。缓解了,但成本飙升。
★★ 翻日志翻出的真相:被打爆的请求,绝大多数是【文章详情页】
和【文档页】—— 而这些页面的内容,几天、几周才改一次。
可 getServerSideProps 让服务器为【每一个访客、每一次刷新】
都把同一篇文章重新查库、重新渲染一遍。
★★ 想明白的根:服务器不是被流量打垮的,是被我写的、海量的
【无谓重复劳动】打垮的 —— 它在千百万次地重复生产
一模一样的东西。
★ 一个内容几乎不变的页面,根本不需要"每次请求都现渲染"。
它只需要被渲染【一次】,之后所有人看的,都是那一份成品。
★ 渲染策略,从来不是"用哪个最方便"的问题,而是"这个页面
到底需不需要实时性、需要多强的实时性"的问题。
★ 本文要做的:把 CSR / SSR / SSG / ISR 四种策略、它们的
取舍、以及怎么按页面去选,彻底讲透。
渲染到底在渲染什么:从"在哪渲染"说起
# === ★ 先把"渲染"这件事,拆到最清楚 ===
# === ★ 一个网页,本质是"一段 HTML"。问题是:谁来生成它 ===
# ★ 你在浏览器里看到的页面,归根结底,是一段 HTML 文本被
# 浏览器解析、显示出来的结果。
# ★ ★ 所谓"渲染策略",讨论的就一件事:这段【最终的 HTML】,
# 是【在什么地方、在什么时间】被生产出来的。
# ★ 就这两个维度 ——【地点】(浏览器 / 服务器 / 构建机器)
# 和【时间】(用户请求时 / 提前构建时)—— 排列组合,
# 就得到了 CSR / SSR / SSG / ISR 这几种策略。
# === ★ 维度一:在"哪里"渲染 ===
# ★ ★ 客户端(浏览器)渲染:服务器只发一个近乎空的 HTML
# 壳子 + 一大坨 JS,真正的页面内容,靠浏览器跑 JS 现拼。
# ★ ★ 服务器渲染:由服务器(或构建机器)把内容【填进
# HTML】,发给浏览器的就是一份内容完整的 HTML。
# === ★ 维度二:在"什么时候"渲染 ===
# ★ ★ 请求时(per-request):用户每发一次请求,才渲染
# 一次。好处是数据永远最新,代价是每次请求都要干活。
# ★ ★ 构建时(build-time):在你部署代码、构建项目的
# 那一刻,就把页面渲染好。好处是用户请求时不用渲染、
# 极快,代价是数据是"构建那一刻"的快照。
# === ★ 四种策略,就是这两个维度的组合 ===
# ★ ★ CSR(客户端渲染):在【浏览器】、在【请求后】渲染。
# ★ ★ SSR(服务器端渲染):在【服务器】、在【每次请求时】
# 渲染。—— 本文事故里我全站用的就是它。
# ★ ★ SSG(静态站点生成):在【构建机器】、在【构建时】
# 渲染。一次渲染好,之后只发静态文件。
# ★ ★ ISR(增量静态再生):SSG 的进化版 —— 构建时先渲染
# 一批,上线后再按需、按时,在后台悄悄地局部"重渲染"。
# === ★ 一个贯穿全文的取舍:新鲜度 vs 性能/成本 ===
# ★ ★ 越靠近"每次请求都渲染"(SSR),数据越新鲜,但
# 服务器越累、响应越慢、成本越高。
# ★ ★ 越靠近"构建时就渲染好"(SSG),响应越快、成本
# 越低,但数据越可能"过时"。
# ★ 这条取舍线,是后面所有选型的【总纲】。没有哪个策略
# "最好",只有"对这个页面,哪个最合适"。
# === 小结 ===
# ★ 一个网页本质是一段 HTML,渲染策略讨论的就一件事:
# 这段最终 HTML 在【什么地点】(浏览器/服务器/构建机器)、
# 【什么时间】(用户请求时/提前构建时)被生产出来。★★ 两
# 维度组合出四种策略:CSR 在浏览器、请求后渲染;SSR 在
# 服务器、每次请求时渲染(本文事故全站用的);SSG 在
# 构建机器、构建时渲染一次之后只发静态文件;ISR 是 SSG
# 进化版,构建时渲染一批、上线后按需按时在后台局部重
# 渲染。★★ 贯穿全文的取舍:越靠近 SSR 数据越新鲜但
# 服务器越累响应越慢成本越高,越靠近 SSG 响应越快成本
# 越低但数据越可能过时 —— 没有哪个策略最好,只有对
# 某个页面哪个最合适。
SSR:每次请求,服务器都现渲染一遍
# === ★ 先讲我栽过跟头的 SSR —— 它到底怎么工作 ===
# === ★ SSR 的流程:请求来了,服务器现做一份"全套" ===
# ★ ★ 用户请求一个页面 -> 服务器收到 -> 服务器【现场】
# 去查数据库 / 调 API,拿到数据 -> 用这些数据,把 React
# 组件渲染成一段【内容完整的 HTML】 -> 把这段 HTML 发回
# 浏览器。
# ★ 浏览器一拿到,立刻就能显示出有内容的页面(不是白屏),
# 随后再加载 JS 做"注水"(hydration),让页面变得可交互。
# === ★ SSR 的两个真实优点 ===
# ★ 优点 1:★★ 数据永远是【请求那一刻】最新的。因为它
# 就是请求时才去查的。
# ★ 优点 2:★ 首屏对 SEO 友好。爬虫一抓,就是一份内容
# 齐全的 HTML,不用等 JS 执行。
# === ★★ SSR 的代价 —— 也就是我被打垮的原因 ===
# ★ ★ 代价 1:服务器【每一次请求都要干一遍重活】。查库、
# 渲染,一次都不能省。请求量一大,服务器 CPU 就被这些
# 重复劳动吃光。
# ★ ★ 代价 2:TTFB 慢。用户要等服务器"查完、渲染完"才
# 能拿到第一个字节。服务器越忙,这个等待越长。
# ★ ★ 代价 3:成本高。要扛住高并发的 SSR,你得堆很多、
# 很强的服务器。
# === ★ 关键:SSR 只该用在"内容真的每次都不同"的页面 ===
# ★ ★ SSR 的全部代价,都是为了换"实时性"。所以它只值得
# 用在【内容确实因人、因时而异】的页面上:
# - ★ 用户的个人中心、订单页(每个用户看到的不一样);
# - ★ 实时性强的仪表盘、股价页;
# - ★ 带个性化推荐、依赖登录态的页面。
# ★ ★★ 而像"文章详情""产品介绍"这种【所有人看到的都
# 一样、且很久才变一次】的页面,用 SSR 就是【纯浪费】
# —— 这正是我那个内容站犯的错。
# === 小结 ===
# ★ SSR 流程:用户请求页面→服务器现场查库/调 API 拿数据→
# 用数据把 React 组件渲染成内容完整的 HTML→发回浏览器,
# 浏览器立刻能显示有内容的页面再加载 JS 注水变可交互。
# ★ 两个优点:① 数据永远是请求那一刻最新的;② 首屏对
# SEO 友好,爬虫抓到就是齐全 HTML。★★ 代价:① 服务器
# 每次请求都要查库渲染一遍重活,量一大 CPU 被吃光;
# ② TTFB 慢,用户要等服务器查完渲染完;③ 成本高,扛
# 高并发 SSR 要堆很多强服务器。★ 关键:SSR 的全部代价
# 都为换实时性,只该用在内容确实因人因时而异的页面 ——
# 个人中心订单页、实时仪表盘股价、个性化推荐依赖登录态
# 的页面;★★ 文章详情产品介绍这种所有人看到都一样且
# 很久才变的页面用 SSR 是纯浪费,正是我那内容站犯的错。
// ★ SSR:Next.js Pages Router —— getServerSideProps
// ★★ 这个函数,在【每一次请求】到来时,都会在服务器上跑一遍
export async function getServerSideProps(context) {
// ★ 每次请求,都现场去查一遍数据 —— 数据永远最新
const { userId } = context.params;
const res = await fetch(`https://api.example.com/users/${userId}`);
const user = await res.json();
// ★ 返回的 props,会被用来在服务器上渲染下面的组件
return { props: { user } };
}
// ★ 页面组件:拿到 props,服务器把它渲染成完整 HTML
export default function UserDashboard({ user }) {
return (
<div>
<h1>{user.name} 的个人中心</h1>
{/* ★★ 这种"每个用户都不同"的页面,才值得用 SSR */}
<p>账户余额:{user.balance}</p>
</div>
);
}
// ★ App Router 的等价写法:fetch 默认就按"动态"处理,
// 或显式声明 export const dynamic = 'force-dynamic'
async function getUser(userId) {
// ★★ cache: 'no-store' —— 告诉 Next:别缓存,每次请求都现拉
const res = await fetch(`https://api.example.com/users/${userId}`,
{ cache: 'no-store' });
return res.json();
}
SSG:构建时渲染一次,之后只发静态文件
# === ★ SSG —— 我那个内容站本该用的东西 ===
# === ★ SSG 的流程:在"构建时"就把页面渲染成死的 HTML ===
# ★ ★ 关键时间点,是你【部署、构建项目】的那一刻。在那
# 一刻,SSG 就把每一个页面,都查好数据、渲染成一份
# 【静态的 .html 文件】。
# ★ ★ 之后,用户来请求 —— 服务器(或 CDN)做的事,只是
# 把那份【早就准备好的 .html 文件】原样发出去。不查库、
# 不渲染、什么都不算。
# === ★ SSG 的优点:快到极致、便宜到极致 ===
# ★ 优点 1:★★ 极快。发的是一个静态文件,TTFB 可以低到
# 几十毫秒。还能直接扔上 CDN,让用户就近拿。
# ★ 优点 2:★★ 极便宜、极抗压。发静态文件几乎不耗服务器
# 资源,流量翻一百倍,一个 CDN 就稳稳扛住。
# ★ 优点 3:★ SEO 同样友好,发出去就是完整 HTML。
# === ★★ SSG 的代价:数据是"构建那一刻"的快照 ===
# ★ ★ 这是 SSG 唯一、但很关键的软肋:页面里的数据,
# 【冻结在你上次构建的那一刻】。
# ★ 你构建完之后,数据库里的内容改了 —— 页面【不会自己
# 变】。想让页面更新,你必须【重新构建一次、重新部署】。
# ★ ★ 如果一个站内容很多(几万篇文章),每次改一个字
# 都要全站重新构建,构建可能要几十分钟 —— 这就是纯
# SSG 在大型内容站上的痛点,也是下一节 ISR 要解决的。
# === ★ SSG 还要解决一个问题:动态路由怎么办 ===
# ★ ★ 像 /posts/[id] 这种动态路由,SSG 在构建时,必须
# 【提前知道到底有哪些 id】,才能挨个把它们渲染成静态
# 文件。所以你要提供一个"路径清单"(getStaticPaths /
# generateStaticParams),告诉构建器:"这些 id,都给我
# 生成出来。"
# === ★ SSG 适合什么页面 ===
# ★ ★ 内容【对所有人都一样】、且【不要求实时更新】的页面:
# 博客文章、产品文档、营销落地页、帮助中心。
# ★ 我那个内容站的文章详情页、文档页,百分之百该用 SSG
# (或下一节的 ISR),而不是 SSR。
# === 小结 ===
# ★ SSG 流程:在你【构建、部署项目】那一刻,就把每个页面
# 查好数据、渲染成静态 .html 文件;之后用户请求,服务器
# 或 CDN 只是把早准备好的 .html 原样发出,不查库不渲染。
# ★ 优点:① 极快,发静态文件 TTFB 几十毫秒还能上 CDN;
# ② 极便宜极抗压,流量翻百倍一个 CDN 稳稳扛;③ SEO
# 友好。★★ 代价是唯一但关键的软肋:数据冻结在上次构建
# 那一刻,构建后数据库改了页面不会自己变,要重新构建
# 重新部署;站内容很多时每改一个字全站重构要几十分钟。
# ★ SSG 还要解决动态路由:/posts/[id] 这种构建时必须提前
# 知道有哪些 id,要提供路径清单(getStaticPaths /
# generateStaticParams)。★ SSG 适合内容对所有人一样
# 且不要求实时更新的页面:博客文章、产品文档、营销落地页。
// ★ SSG:Next.js Pages Router —— getStaticProps + getStaticPaths
// ★★ getStaticProps 只在【构建时】跑,跑完页面就被渲染成死的 HTML
export async function getStaticProps(context) {
const { id } = context.params;
const res = await fetch(`https://api.example.com/posts/${id}`);
const post = await res.json();
return { props: { post } };
}
// ★★ 动态路由 /posts/[id]:必须告诉构建器"到底有哪些 id"
export async function getStaticPaths() {
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();
return {
// ★ 这份清单里的每个 id,构建时都会被生成成一个静态文件
paths: posts.map((p) => ({ params: { id: String(p.id) } })),
// ★ fallback: false —— 清单外的 id 一律 404
fallback: false,
};
}
export default function Post({ post }) {
return (
<article>
<h1>{post.title}</h1>
{/* ★★ 文章详情:所有人看到都一样、很久才变 —— SSG 的主场 */}
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
// ★ App Router 的等价写法:generateStaticParams 提供路径清单
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then((r) => r.json());
return posts.map((p) => ({ id: String(p.id) }));
}
ISR:让静态页面"自己悄悄更新"
# === ★ ISR —— SSG 的痛点"数据会过时",它来解决 ===
# === ★ ISR 想解决的那个矛盾 ===
# ★ ★ 纯 SSG:快、便宜,但数据冻结,改内容要全站重构。
# ★ ★ 纯 SSR:数据新鲜,但慢、贵、每次请求都干重活。
# ★ ISR(Incremental Static Regeneration,增量静态再生)
# 想要的是:【绝大多数时候享受 SSG 的快和便宜,同时让
# 页面能自动、增量地保持新鲜】—— 取两者的甜区。
# === ★ ISR 的核心机制:revalidate(过期再生)===
# ★ ★ 你给一个静态页面,设一个 revalidate 时间(比如
# 60 秒)。它的工作方式是:
# - ① 页面像 SSG 一样,先有一份构建好的静态版本;
# - ② 用户来访问,直接拿到这份静态版本 —— ★ 飞快;
# - ③ 距上次生成【超过 60 秒后】的第一个访问者,依然
# 【先拿到那份旧的静态页】(他不用等);
# - ④ 但与此同时,服务器在【后台】悄悄地,用最新数据
# 把这个页面【重新生成】一份;
# - ⑤ 生成好后,把旧版本替换掉。之后的访问者,拿到的
# 就是【新版本】了。
# ★ ★★ 精髓:更新是在【后台异步】发生的,没有任何一个
# 用户,需要为"重新渲染"去等待。页面始终是静态的快,
# 数据又能在 revalidate 周期内自动刷新。
# === ★ ISR 还有一招:按需再生(On-Demand Revalidation)===
# ★ ★ 光靠"定时过期"还不够灵敏。ISR 还允许你【主动触发】
# 某个页面的再生。
# ★ 典型用法:你在 CMS 后台改了一篇文章、点了"发布" ——
# CMS 调用一个你写的接口,这个接口告诉 Next:"/posts/123
# 这个页面,立刻给我重新生成。"
# ★ ★ 这样,内容一更新,页面【秒级】就刷新了,既不用
# 傻等 revalidate 周期,也不用全站重构。这是大型内容
# 站最理想的姿势。
# === ★ ISR 适合什么 ===
# ★ ★ 内容【对所有人一样】,但【会不定期更新】,且能
# 【容忍最多几十秒、几分钟的延迟】的页面:博客、新闻、
# 电商的商品详情页(价格库存可容忍短暂延迟时)。
# ★ 它几乎是"内容型页面"的默认最优解 —— 我那个内容站,
# 正确答案就是 ISR。
# === 小结 ===
# ★ ISR 解决的矛盾:纯 SSG 快便宜但数据冻结改内容要全站
# 重构,纯 SSR 数据新鲜但慢贵每次请求干重活 —— ISR 要
# 绝大多数时候享受 SSG 的快和便宜、同时让页面自动增量
# 保持新鲜,取两者甜区。★★ 核心机制 revalidate:给静态
# 页设过期时间(如 60 秒),用户始终直接拿静态版本飞快,
# 超过 60 秒后第一个访问者仍先拿旧页不用等,同时服务器
# 在后台用最新数据悄悄重新生成、好了替换掉,之后的访问
# 者拿到新版本 —— 精髓是更新在后台异步发生没有用户为
# 重渲染等待。★★ 另一招按需再生:在 CMS 改完文章点发布
# 时调一个接口主动触发某页面立刻重新生成,内容一更新
# 页面秒级刷新,不用等周期也不用全站重构,是大型内容站
# 最理想姿势。★ ISR 适合内容对所有人一样、会不定期更新、
# 能容忍几十秒几分钟延迟的页面 —— 几乎是内容型页面的
# 默认最优解。
// ★ ISR:Pages Router —— getStaticProps 里加一个 revalidate
export async function getStaticProps(context) {
const { id } = context.params;
const post = await fetch(`https://api.example.com/posts/${id}`)
.then((r) => r.json());
return {
props: { post },
// ★★ 关键就这一行:页面每 60 秒"过期",过期后
// 下一个访问者触发后台重新生成 —— 他自己仍拿旧页不等
revalidate: 60,
};
}
// ★★ getStaticPaths 配 fallback: 'blocking' —— 清单外的新 id
// 首次访问时现生成一次,生成后也变成静态页(适合文章不断新增)
export async function getStaticPaths() {
const posts = await fetch('https://api.example.com/posts/hot')
.then((r) => r.json());
return {
paths: posts.map((p) => ({ params: { id: String(p.id) } })),
fallback: 'blocking',
};
}
// ★ App Router 的等价写法:用 fetch 的 next.revalidate 选项
async function getPost(id) {
const res = await fetch(`https://api.example.com/posts/${id}`,
{ next: { revalidate: 60 } }); // ★ 60 秒 ISR
return res.json();
}
// ★ ISR 的"按需再生":写一个接口,让 CMS 发布时主动触发刷新
// ★ App Router:app/api/revalidate/route.js
import { revalidatePath } from 'next/cache';
import { NextResponse } from 'next/server';
export async function POST(request) {
const { searchParams } = new URL(request.url);
const secret = searchParams.get('secret');
const path = searchParams.get('path');
// ★★ 必须校验密钥 —— 否则任何人都能狂刷你的再生接口
if (secret !== process.env.REVALIDATE_SECRET) {
return NextResponse.json({ error: '无权限' }, { status: 401 });
}
if (!path) {
return NextResponse.json({ error: '缺少 path' }, { status: 400 });
}
// ★ 立刻让指定页面失效 —— 下次访问就会拿到重新生成的新版
revalidatePath(path);
return NextResponse.json({ revalidated: true, path });
}
// ★ CMS 后台"发布文章"后,调一下:
// POST /api/revalidate?secret=xxx&path=/posts/123
// —— 内容秒级刷新,既不等 revalidate 周期,也不全站重构
怎么选:一棵"按页面"走的决策树
# === ★ 把四种策略,串成一个能直接用的决策流程 ===
# === ★★ 第一步,也是最重要的一步:别再"全站统一" ===
# ★ ★ 我那个内容站最大的错,不是"选了 SSR",而是
# 【全站只用一种策略】。
# ★ 一个真实的站点,不同页面的需求天差地别:首页、文章页、
# 个人中心、后台、登录页 —— 它们对"实时性"的要求完全
# 不同。★★ 渲染策略,必须【一个页面一个页面地选】。
# === ★ 决策树:对每一个页面,依次问下面几个问题 ===
# ★ 问题 1:这个页面的内容,【是不是每个用户都不同 / 依赖
# 登录态 / 必须是实时的】?
# - ★★ 是 -> 用 SSR。(个人中心、订单、实时仪表盘)
# - 否 -> 往下问。
# ★ 问题 2(走到这,说明内容对所有人一样):这个页面的
# 内容,【会更新吗】?
# - ★★ 几乎永不更新(关于我们、一篇定稿的文档)
# -> 用 SSG。构建一次,永久静态,最快最便宜。
# - 会不定期更新 -> 往下问。
# ★ 问题 3:内容更新后,【能容忍多久的延迟】才让用户
# 看到新内容?
# - ★★ 能容忍几十秒 / 几分钟的延迟 -> 用 ISR。
# (博客、新闻、商品详情)—— 这是内容型页面的甜区。
# - ★ 一秒都不能差(秒杀实时库存、竞价)-> 回到 SSR。
# === ★ 那 CSR(纯客户端渲染)什么时候用 ===
# ★ ★ CSR 不太适合"需要 SEO 的、面向公众的页面"(首屏
# 是空壳,不友好)。但它非常适合:
# - ★ 登录后才能看的【后台管理系统】—— 不需要 SEO,
# 强交互,数据高度动态;
# - ★ 一个本质是 SSR/SSG 页面里的【某个高度动态的局部
# 组件】(如一个实时聊天窗) —— 页面用 SSG,这个组件
# 单独用 CSR 在浏览器里拉数据。
# ★ ★★ 现代框架不是"四选一",而是【混合】:一个页面
# 可以是 SSG 的骨架 + 几个 CSR 的动态岛。
# === ★ 一条容易被忽略的提醒:SEO 和首屏 ===
# ★ 面向搜索引擎、面向公众的页面(落地页、文章),首屏
# 一定要有真实内容 -> 倾向 SSG / ISR / SSR,别用纯 CSR。
# ★ 纯 CSR 的首屏是空的,爬虫和慢网速用户都会受伤。
# === 认知 ===
# ★ 选型第一步也是最重要一步:别再全站统一 —— 我那内容站
# 最大的错不是选了 SSR,是全站只用一种策略;真实站点
# 首页文章页个人中心后台登录页对实时性要求天差地别,
# 渲染策略必须一个页面一个页面地选。★ 决策树对每页依次
# 问:① 内容是不是每个用户都不同/依赖登录态/必须实时?
# 是→SSR;② 否(对所有人一样)那会更新吗?几乎永不更新
# →SSG;③ 会不定期更新那能容忍多久延迟?能容忍几十秒
# 几分钟→ISR(内容型页面甜区),一秒都不能差→回 SSR。
# ★ CSR 不适合需要 SEO 的公众页面(首屏空壳),但很适合
# 登录后的后台管理系统(不需 SEO、强交互、数据动态)、
# 或 SSG/SSR 页面里某个高度动态的局部组件;★★ 现代框架
# 不是四选一而是混合 —— 一个页面可以是 SSG 骨架+几个
# CSR 动态岛。★ 别忽略 SEO:面向搜索引擎和公众的页面
# 首屏一定要有真实内容,倾向 SSG/ISR/SSR 别用纯 CSR。
工程选型:真实项目里怎么混用,以及那些坑
# === ★ Demo 易写,真实项目里把渲染策略用对,要注意这些 ===
# === ★ 要点 1:一个站,就该是多种策略的混合体 ===
# ★ ★ 以我那个内容站为例,正确的选型应该是:
# - 首页(内容会变、对所有人一样)-> ISR;
# - 文章详情页、文档页 -> ISR(配按需再生);
# - "关于我们"等定稿页 -> SSG;
# - 用户的个人中心、阅读历史 -> SSR;
# - 登录后的写作后台 -> CSR。
# ★ 一个 Next.js 项目,完全可以让不同路由用不同策略 ——
# 这正是它的设计意图。
# === ★★ 要点 2:ISR 的 revalidate 时间,要权衡着设 ===
# ★ revalidate 设太短(如 5 秒),后台再生太频繁,失去了
# 静态的成本优势,接近 SSR 了。
# ★ revalidate 设太长(如 1 天),内容更新后用户要等很久。
# ★ ★ 正解:revalidate 设一个"能接受的延迟"的较大值
# (如几分钟到一小时),把"立刻要新"的需求,交给
# 【按需再生】去精确触发。两者配合,才是 ISR 的正确用法。
# === ★ 要点 3:数据获取的耗时,SSR 下会被直接放大 ===
# ★ ★ SSR 页面里,如果你串行地 await 了好几个慢接口,
# 这些耗时会【全部累加】进 TTFB,用户干等。
# ★ 对策:SSR 里的多个数据请求,无依赖的要【并行】
# (Promise.all);能不在服务端拉的、非首屏必需的数据,
# 挪到客户端去拉。
# === ★ 要点 4:别忘了"缓存"这个常常被忽略的中间层 ===
# ★ ★ 就算一个页面必须用 SSR,你也可以在它前面加一层
# CDN / 边缘缓存:给响应设一个短的 Cache-Control
# (如 stale-while-revalidate)。这能让高并发下的重复
# 请求,不必每个都穿透到你的渲染服务器。
# ★ 很多"SSR 扛不住"的场景,加一层合理的缓存就缓解了。
# === ★★ 要点 5:构建时间,是大型 SSG/ISR 站的隐形成本 ===
# ★ ★ 如果用 SSG 把几万个页面【全部】在构建时生成,构建
# 会慢到无法忍受。
# ★ 对策:用 ISR 的 fallback: 'blocking' —— 构建时只
# 预生成【最热门的一批】页面,长尾页面留到"首次被访问
# 时"再现场生成一次(之后它也变静态)。构建快,覆盖也全。
# === ★ 要点 6:渲染策略选错的代价,是"延迟到线上"的 ===
# ★ ★ 这是我最深的教训:开发和测试时,流量小,SSR、SSG
# 表现【几乎没差别】,你根本测不出选错了。
# ★ ★ 选错的代价,会一直【潜伏到线上流量真正上来的那
# 一天】,以事故的形式,一次性引爆。所以渲染策略要在
# 【设计期】就照决策树想清楚,别留到线上。
# === 认知 ===
# ★ 真实项目要点:① 一个站就该是多种策略的混合体 —— 首页
# 和文章页 ISR、定稿页 SSG、个人中心 SSR、写作后台 CSR,
# Next.js 本就支持不同路由用不同策略;② ISR 的 revalidate
# 要权衡:太短(5 秒)后台再生太频繁失去成本优势接近
# SSR,太长(1 天)内容更新用户等太久,正解是设一个能
# 接受延迟的较大值再把"立刻要新"交给按需再生精确触发;
# ③ SSR 下数据获取耗时会直接累加进 TTFB,无依赖的多个
# 请求要并行 Promise.all、非首屏必需的数据挪客户端拉;
# ④ 别忘缓存中间层,SSR 页面前加一层 CDN/边缘缓存设短
# Cache-Control,高并发重复请求就不必每个都穿透到渲染
# 服务器;⑤ 构建时间是大型 SSG/ISR 站隐形成本,几万
# 页全在构建时生成会慢到无法忍受,用 fallback:'blocking'
# 只预生成最热一批、长尾留到首次访问现场生成;⑥ 最深
# 教训 —— 渲染策略选错的代价是延迟到线上的,开发测试
# 时流量小 SSR 和 SSG 几乎没差别测不出,选错会潜伏到
# 线上流量上来那天一次性引爆,所以要在设计期就照决策树
# 想清楚。
// ★ 真实项目:同一个 Next.js App Router 项目,不同路由不同策略
// ① app/page.js —— 首页:内容会变、对所有人一样 -> ISR
async function getHomeData() {
const res = await fetch('https://api.example.com/home',
{ next: { revalidate: 300 } }); // ★ ISR 5 分钟
return res.json();
}
// ② app/about/page.js —— 关于我们:定稿页 -> SSG(默认就是)
async function getAbout() {
// ★ fetch 默认会被缓存 = 构建时取一次,之后静态。这就是 SSG
const res = await fetch('https://api.example.com/about');
return res.json();
}
// ③ app/dashboard/page.js —— 个人中心:依赖登录态 -> SSR
export const dynamic = 'force-dynamic'; // ★★ 强制每次请求都渲染
async function getDashboard() {
const res = await fetch('https://api.example.com/me',
{ cache: 'no-store' }); // ★ 不缓存
return res.json();
}
// ④ SSR 页面里的数据获取:无依赖的请求,务必并行,别串行 await
async function getDashboardFast() {
// ★★ 并行 —— 三个接口的耗时取最大值,而不是累加
const [me, orders, msgs] = await Promise.all([
fetch('https://api.example.com/me', { cache: 'no-store' }).then((r) => r.json()),
fetch('https://api.example.com/orders', { cache: 'no-store' }).then((r) => r.json()),
fetch('https://api.example.com/messages', { cache: 'no-store' }).then((r) => r.json()),
]);
return { me, orders, msgs };
}
命令速查
四种渲染策略对比
=============================================================
策略 渲染地点 渲染时机 数据新鲜度 速度/成本
CSR 浏览器 请求后(跑 JS) 动态 首屏慢 服务器省
SSR 服务器 每次请求时 永远最新 慢 贵 抗压差
SSG 构建机器 构建时(一次) 构建那一刻 极快 极便宜
ISR 构建+后台 构建时+按需再生 周期内新鲜 快 便宜 且新鲜
Next.js 写法速查
-------------------------------------------------------------
SSR Pages: getServerSideProps App: cache:'no-store' / dynamic
SSG Pages: getStaticProps App: fetch 默认缓存
ISR Pages: getStaticProps + revalidate: 60
App: fetch(url,{ next:{ revalidate:60 } })
按需再生 revalidatePath(path) —— CMS 发布时调接口触发
动态路由 getStaticPaths / generateStaticParams 提供路径清单
决策树(对每个页面单独走一遍)
-------------------------------------------------------------
每人不同/依赖登录/必须实时 -> SSR
对所有人一样 + 几乎不更新 -> SSG
对所有人一样 + 不定期更新 + 容忍延迟 -> ISR
登录后的后台/强交互局部组件 -> CSR
口诀:渲染策略一个页面一个页面地选,绝不全站统一
内容型页面默认 ISR,定稿页 SSG,个性化页 SSR
选错的代价不在开发期,而在线上流量上来那天引爆
避坑清单
- 渲染策略讨论的就是"最终 HTML 在什么地点、什么时间被生成",别凭"哪个最方便"选,要凭"这页需不需要实时性"选
- 全站统一一种渲染策略是最大的错,首页文章页个人中心后台对实时性要求天差地别,必须一个页面一个页面地选
- SSR 让服务器为每个访客每次刷新都重新查库渲染一遍,内容很久才变的页面用 SSR 是纯浪费,流量一大就被重复劳动打垮
- SSG 在构建时把页面渲染成死的静态文件,极快极便宜极抗压,但数据冻结在上次构建那一刻,改内容要重新构建部署
- 动态路由用 SSG/SSG 时必须用 getStaticPaths 或 generateStaticParams 提前告诉构建器到底有哪些 id
- ISR 的精髓是过期后的访问者仍先拿旧静态页不等待,服务器在后台异步重新生成,没有用户为重渲染买单
- ISR 的 revalidate 设太短会接近 SSR 失去成本优势,设太长内容更新太慢,正解是设较大值再配按需再生精确触发
- 按需再生接口必须校验密钥,否则任何人都能狂刷你的再生接口,把服务器压力打回 SSR 的水平
- SSR 页面里多个无依赖的数据请求要用 Promise.all 并行,串行 await 会把所有耗时累加进 TTFB 让用户干等
- 渲染策略选错开发测试时流量小几乎测不出,代价会潜伏到线上流量上来那天一次性引爆,必须在设计期就想清楚
总结
这一趟把渲染策略彻底理清的过程,纠正了我一个关于"简单"的、藏得极深的错觉。在我那个内容站被流量打垮之前,我对"全站统一用 getServerSideProps"这个决定,心里是有一份得意的:我觉得这叫【简单】、叫【一致】、叫【不折腾】——我不用为每个页面纠结"到底用哪种",我用一种最万能的,一了百了。我把"统一",当成了一种工程上的美德。直到那场事故让我看清:我所谓的"简单",根本不是简单,它只是【把思考省掉了】。我没有为任何一个页面,认真想过它"到底需要什么"——文章页需要的是"快和便宜",我却给了它"昂贵的实时性";它根本不需要服务器每次都为它重渲染,我却逼着服务器这么做。我以为我做了一个省事的决定,其实我只是把"该在设计期做的思考",打包推迟了——推迟到了线上,推迟成了一场事故,然后用扩容的真金白银,去偿还我当初省下的那点思考。复盘到最深,我意识到 CSR / SSR / SSG / ISR 这四个词,真正教给我的,不是几种 Next.js 的 API 用法,而是一件更朴素的事:【没有一种策略是"最好"的,每一种,都只是一组明确的取舍】。SSR 用服务器的辛劳和金钱,换数据的实时;SSG 用数据的"过时风险",换极致的快和便宜;ISR 在两者之间,找了一个精巧的平衡点。它们谁也不比谁高级——它们只是【为不同的需求,准备的不同工具】。而我的工作,从来不是去找"那个最好的",而是要先沉下心,为我的【每一个页面】,诚实地回答一个问题:"它,到底需要什么?"——这个页面的内容,是给一个人看的,还是给所有人看的?它变得勤,还是变得懒?它变了以后,用户等一分钟才看到新的,要紧吗?把这几个问题想清楚了,该用哪种策略,答案【自己就浮现出来了】。这个认知,后来改变的远不止我写前端的方式。我开始警惕一切形式的"为了省事而做的统一"——统一的技术栈、统一的数据库、统一的缓存时间、统一的超时配置。"统一",听起来整洁、可控、有纪律,但它常常掩盖着一个真相:我们没有为每一个具体的场景,做过具体的思考。真正的工程素养,恰恰相反:它不怕"不统一",它怕的是"没想过"。它愿意为首页、文章页、个人中心,分别选三种不同的渲染策略,因为它清楚地知道,这三个页面,本就是三种不同的东西。这次最大的收获,是我换掉了对"简单"的定义。我过去以为,简单是"所有地方都用同一种做法";现在我明白,真正的简单,是"每一个地方,都用了那个【最恰如其分】的做法"——它看起来或许没那么"统一",但它没有任何一处是浪费的,没有任何一处是将就的,因此它在真实的流量面前,稳得住。那个被打垮的内容站教给我的,从来不是一个性能优化的技巧,而是一句让我受用很久的话:不要用"统一"去逃避"思考";你为一个系统省下的每一分该做的思考,它都会在未来的某一天,连本带利地,向你讨回去。
—— 别看了 · 2026