SSR / SSG / ISR 完全指南:现代渲染策略的工程选型

2023 年我用 Next.js 做了个内容站(技术博客+产品文档),开发时图省事:页面要数据就写 getServerSideProps,它最万能每次请求都现拉最新数据,我索性全站每个页面都用它。本地飞快、上线初期流量小一切正常,直到市场部一波推广让流量一小时翻几十倍 —— 页面 TTFB 从 200ms 飙到 4 秒,服务器 CPU 焊死 100%,大面积 502,用户白屏等四五秒大量流失。我第一反应是加机器,缓解了但成本飙升。翻日志才僵住:被打爆的请求绝大多数是文章详情页和文档页,而这些内容几天几周才改一次,可我的代码却让服务器为每个访客每次刷新都把同一篇文章重新查库重新用 React 渲染一遍 —— 服务器不是被流量打垮的,是被我写的海量无谓重复劳动打垮的。梳理:渲染策略讨论的就一件事,最终 HTML 在什么地点(浏览器/服务器/构建机器)、什么时间(请求时/构建时)被生成,两维度组合出四种。CSR 在浏览器请求后跑 JS 渲染;SSR 在服务器每次请求时渲染,数据永远最新且 SEO 友好,但每次请求都要查库渲染一遍重活、TTFB 慢、成本高,只该用在内容因人因时而异的页面(个人中心、订单、实时仪表盘);SSG 在构建时把每个页面渲染成死的静态 .html 文件,之后只发静态文件极快极便宜极抗压,但数据冻结在上次构建那一刻,改内容要重新构建部署;ISR 是 SSG 进化版,给静态页设 revalidate 时间,用户始终直接拿静态版本飞快,过期后第一个访问者仍先拿旧页不等、服务器在后台异步用最新数据重新生成好了替换,还能用按需再生在 CMS 发布时调接口让页面秒级刷新。决策树对每个页面单独走:内容每人不同/依赖登录/必须实时→SSR,对所有人一样且几乎不更新→SSG,对所有人一样会不定期更新且能容忍几十秒几分钟延迟→ISR(内容型页面甜区),登录后的后台/强交互局部组件→CSR。最大的错不是选了 SSR 而是全站只用一种策略,渲染策略必须一个页面一个页面地选 —— 一个 Next.js 项目首页和文章页 ISR、定稿页 SSG、个人中心 SSR、写作后台 CSR。ISR 的 revalidate 太短接近 SSR 失去成本优势、太长内容更新太慢,正解是设较大值再配按需再生精确触发;SSR 下多个无依赖请求要 Promise.all 并行别串行 await 累加进 TTFB;选错的代价开发测试时流量小测不出,会潜伏到线上流量上来那天一次性引爆。正确做法是不要用"统一"去逃避"思考",真正的简单不是所有地方用同一种做法,而是每个地方都用了最恰如其分的做法,你为系统省下的每一分该做的思考它都会在未来连本带利向你讨回去。

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
      选错的代价不在开发期,而在线上流量上来那天引爆

避坑清单

  1. 渲染策略讨论的就是"最终 HTML 在什么地点、什么时间被生成",别凭"哪个最方便"选,要凭"这页需不需要实时性"选
  2. 全站统一一种渲染策略是最大的错,首页文章页个人中心后台对实时性要求天差地别,必须一个页面一个页面地选
  3. SSR 让服务器为每个访客每次刷新都重新查库渲染一遍,内容很久才变的页面用 SSR 是纯浪费,流量一大就被重复劳动打垮
  4. SSG 在构建时把页面渲染成死的静态文件,极快极便宜极抗压,但数据冻结在上次构建那一刻,改内容要重新构建部署
  5. 动态路由用 SSG/SSG 时必须用 getStaticPaths 或 generateStaticParams 提前告诉构建器到底有哪些 id
  6. ISR 的精髓是过期后的访问者仍先拿旧静态页不等待,服务器在后台异步重新生成,没有用户为重渲染买单
  7. ISR 的 revalidate 设太短会接近 SSR 失去成本优势,设太长内容更新太慢,正解是设较大值再配按需再生精确触发
  8. 按需再生接口必须校验密钥,否则任何人都能狂刷你的再生接口,把服务器压力打回 SSR 的水平
  9. SSR 页面里多个无依赖的数据请求要用 Promise.all 并行,串行 await 会把所有耗时累加进 TTFB 让用户干等
  10. 渲染策略选错开发测试时流量小几乎测不出,代价会潜伏到线上流量上来那天一次性引爆,必须在设计期就想清楚

总结

这一趟把渲染策略彻底理清的过程,纠正了我一个关于"简单"的、藏得极深的错觉。在我那个内容站被流量打垮之前,我对"全站统一用 getServerSideProps"这个决定,心里是有一份得意的:我觉得这叫【简单】、叫【一致】、叫【不折腾】——我不用为每个页面纠结"到底用哪种",我用一种最万能的,一了百了。我把"统一",当成了一种工程上的美德。直到那场事故让我看清:我所谓的"简单",根本不是简单,它只是【把思考省掉了】。我没有为任何一个页面,认真想过它"到底需要什么"——文章页需要的是"快和便宜",我却给了它"昂贵的实时性";它根本不需要服务器每次都为它重渲染,我却逼着服务器这么做。我以为我做了一个省事的决定,其实我只是把"该在设计期做的思考",打包推迟了——推迟到了线上,推迟成了一场事故,然后用扩容的真金白银,去偿还我当初省下的那点思考。复盘到最深,我意识到 CSR / SSR / SSG / ISR 这四个词,真正教给我的,不是几种 Next.js 的 API 用法,而是一件更朴素的事:【没有一种策略是"最好"的,每一种,都只是一组明确的取舍】。SSR 用服务器的辛劳和金钱,换数据的实时;SSG 用数据的"过时风险",换极致的快和便宜;ISR 在两者之间,找了一个精巧的平衡点。它们谁也不比谁高级——它们只是【为不同的需求,准备的不同工具】。而我的工作,从来不是去找"那个最好的",而是要先沉下心,为我的【每一个页面】,诚实地回答一个问题:"它,到底需要什么?"——这个页面的内容,是给一个人看的,还是给所有人看的?它变得勤,还是变得懒?它变了以后,用户等一分钟才看到新的,要紧吗?把这几个问题想清楚了,该用哪种策略,答案【自己就浮现出来了】。这个认知,后来改变的远不止我写前端的方式。我开始警惕一切形式的"为了省事而做的统一"——统一的技术栈、统一的数据库、统一的缓存时间、统一的超时配置。"统一",听起来整洁、可控、有纪律,但它常常掩盖着一个真相:我们没有为每一个具体的场景,做过具体的思考。真正的工程素养,恰恰相反:它不怕"不统一",它怕的是"没想过"。它愿意为首页、文章页、个人中心,分别选三种不同的渲染策略,因为它清楚地知道,这三个页面,本就是三种不同的东西。这次最大的收获,是我换掉了对"简单"的定义。我过去以为,简单是"所有地方都用同一种做法";现在我明白,真正的简单,是"每一个地方,都用了那个【最恰如其分】的做法"——它看起来或许没那么"统一",但它没有任何一处是浪费的,没有任何一处是将就的,因此它在真实的流量面前,稳得住。那个被打垮的内容站教给我的,从来不是一个性能优化的技巧,而是一句让我受用很久的话:不要用"统一"去逃避"思考";你为一个系统省下的每一分该做的思考,它都会在未来的某一天,连本带利地,向你讨回去。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

Prompt 注入完全指南:大模型应用的头号安全漏洞

2026-5-21 12:36:57

技术教程

RAG 完全指南:给大模型外挂一个企业知识库的正确姿势

2026-5-21 12:51:36

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索