# next渲染策略与缓存机制

# 为什么 next 的渲染策略是核心能力

Next 真正强的点不是“能 SSR”,而是可以在同一项目里组合多种策略,并通过缓存体系把性能和一致性做平衡。

如果你是资深前端,建议把 Next 的性能模型拆成三层看:

  • 渲染层:SSR/SSG/ISR/Streaming
  • 缓存层:请求缓存、页面缓存、CDN 缓存
  • 失效层:时间失效、事件失效、标签失效

# 四种常见渲染形态

# 1)CSR(客户端渲染)

首屏壳子在浏览器里再拉数据。
适合强交互后台,不适合 SEO 首屏要求高的内容页。

# 2)SSR(服务端请求时渲染)

每次请求都在服务端算页面,数据新鲜度高。
代价是服务端计算成本更高。

# 3)SSG(构建时静态生成)

构建阶段把页面提前生成静态文件。
访问快、成本低,适合文档、营销页。

# 4)ISR(增量静态再生成)

首次静态,过期后后台重建,兼顾性能与新鲜度。


# 渲染策略选择矩阵(生产决策版)

# 内容型页面(新闻/博客/文档)

  • 推荐:SSG + ISR
  • 目标:高命中缓存、低服务端成本

# 交易型页面(订单、库存、价格)

  • 推荐:SSR + 局部缓存
  • 目标:数据新鲜优先,避免陈旧业务数据

# 强交互后台(报表、管理台)

  • 推荐:CSR/SSR 混合
  • 目标:交互体验与权限控制优先

# 混合页面(电商详情)

  • 推荐:Server Components + Suspense + 局部 Client
  • 目标:首屏快、交互区灵活、可控新鲜度

# App Router 下的数据缓存语义

fetch 在服务端默认可缓存(取决于配置),常见参数:

await fetch(url, {
  next: { revalidate: 60, tags: ["post-list"] },
});
1
2
3

# 关键点

  • revalidate:按时间重验证
  • tags:按标签失效(配合 revalidateTag)
  • cache:控制请求缓存行为

# cache 常见模式(重要)

  • force-cache:尽量走缓存(内容站常用)
  • no-store:每次都拉新(强实时业务)
  • next.revalidate:在缓存基础上给出再验证窗口

# 你需要理解的“多层缓存”

很多同学把 Next 缓存只看成 fetch 缓存,实际上至少有三层:

  1. 数据缓存(fetch 层)
  2. 路由输出缓存(页面/片段层)
  3. 边缘/CDN 缓存(分发层)

线上问题通常出在“层间不一致”,不是某一层配置错了。


# 一个常见策略组合(内容站)

  • 首页列表:revalidate: 60
  • 详情页:revalidate: 300
  • 后台发布后:调用 revalidateTag("post-list")

收益:

  • 用户看的是近实时数据
  • 服务端不必每次都全量重算

# 失效策略设计:时间驱动 vs 事件驱动

# 时间驱动(TTL)

适合“允许短时间旧数据”的场景,如资讯列表。
优点是简单稳定,缺点是存在窗口期。

# 事件驱动(Tag/Path Revalidate)

内容发布、价格变更后主动触发失效。
优点是更实时,缺点是需要保证发布链路可靠触发。

# 生产建议

  • 列表页用 revalidate + tag 组合
  • 关键详情页可缩短 revalidate 并叠加事件失效
  • 写操作完成后在同事务语义里触发 revalidate,避免“写成功但缓存未失效”

# 动态与静态边界判断

当你使用如下能力时,路由更可能走动态渲染:

  • cookies() / headers()
  • 与用户会话强绑定的数据
  • 明确设置 dynamic = "force-dynamic"

建议:把“必须动态”的逻辑隔离到最小范围,避免整页退化为动态。


# Streaming 与缓存的配合

很多页面慢不是“SSR 慢”,而是“等待所有数据再输出”。
配合 Suspense 分段输出后,即使某块慢数据未返回,首屏壳和核心信息也可先到达。

建议把页面拆成:

  • 首屏关键区:快接口 + 稳定缓存
  • 次要区块:慢接口 + Suspense fallback

这样可以同时优化 TTFB 和 FCP 体感。


# 案例:文章列表页(带缓存与重验证)

app/posts/page.tsx

async function getPosts() {
  const res = await fetch("https://api.example.com/posts", {
    next: { revalidate: 60, tags: ["posts"] },
  });
  return res.json();
}

export default async function Page() {
  const posts = await getPosts();
  return (
    <ul>
      {posts.map((p: any) => (
        <li key={p.id}>{p.title}</li>
      ))}
    </ul>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

发布后在服务端触发:

import { revalidateTag } from "next/cache";

export async function POST() {
  revalidateTag("posts");
  return Response.json({ ok: true });
}
1
2
3
4
5
6

# 进阶:按租户隔离缓存标签

多租户场景不要只用 "posts",建议标签带租户前缀:

const tag = `tenant:${tenantId}:posts`;
1

否则会出现租户 A 发布导致租户 B 缓存被误失效。


# 案例:商品详情(价格实时 + 详情缓存)

需求:

  • 商品描述可以 5 分钟缓存
  • 价格库存必须实时

实现思路:

  • 描述信息:revalidate: 300
  • 价格库存:cache: "no-store"
  • UI 上用 Suspense 把价格区块独立,避免拖慢整页输出

这类“冷热数据分治”是 Next 项目最常见性能抓手。


# 观测与压测建议(别只看 Lighthouse)

至少监控:

  • TTFB(服务端首字节)
  • HTML 大小与 RSC Payload 大小
  • 缓存命中率(数据层、CDN 层)
  • 页面级 revalidate 触发次数

压测时要做两类流量:

  • 冷启动流量(无缓存)
  • 稳态流量(高缓存命中)

两者差异能直接反映你缓存策略是否有效。


# 常见误区

  • 误区 1:所有页面都 SSR 才“高级”
  • 误区 2:看见缓存就担心脏数据,不做失效策略
  • 误区 3:把用户态数据和公共数据混用同一缓存策略
  • 误区 4:只在开发环境验证缓存行为(dev 与 prod 行为不同)
  • 误区 5:把 revalidate 当实时系统替代方案

# 小结

Next 渲染策略的正确打开方式是“组合拳”:
静态优先 + 动态兜底 + 标签化失效。
这比单纯讨论 SSR/CSR 更接近真实生产场景。