Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← Web 开发 / Web Development

React 生态 / React Ecosystem

1. React 生态知识体系 / React Ecosystem Knowledge System

2. React 组件渲染、协调与数据流 / React Components, Rendering, and Data Flow

3. Hooks 深度剖析:状态、副作用与并发模式 / Hooks Deep Dive: State, Effects, and Concurrency

4. 路由、表单与组件架构 / Routing, Forms, and Component Architecture

5. React 状态管理:Redux Toolkit 与 Zustand / React State Management: Redux Toolkit and Zustand

6. Next.js App Router:路由、渲染与工程边界 / Next.js App Router, Routing, Rendering, and Engineering Boundaries

7. Next.js 数据获取、缓存与变更 / Next.js Data Fetching, Caching, and Mutations

8. TanStack Query 与服务端状态管理 / TanStack Query and Server State Management

9. React 测试、性能与生产工程 / React Testing, Performance, and Production Engineering

本页目录

Next.js 数据获取、缓存与变更 / Next.js Data Fetching, Caching, and Mutations ​

📅 创建时间:2026-07-28 🏷️ 标签:#Nextjs #Cache #DataFetching #ServerActions #ISR 📚 前置知识:[[05-nextjs-app-router-and-rendering]]

📋 本章目标 ​

  • 理解 Next.js 四层缓存体系的架构、作用范围与失效机制
  • 掌握 fetch 缓存策略、revalidatePath 与 revalidateTag 的按需清除
  • 区分 ISR 按需重新验证与时间窗口重新验证,理解其适用场景
  • 编写安全的 Server Action,理解其序列化限制与授权模型
  • 掌握 useOptimistic 乐观更新模式及其错误回滚策略
  • 理解 React cache() 与 unstable_cache 的层次关系
  • 识别缓存过度、noStore 误用与 Router Cache 行为等常见陷阱

第1部分:四层缓存体系图解 ​

Next.js 的缓存不是"一层缓存",而是四层相互独立的缓存层叠加。理解每一层的作用范围、生命周期和失效条件,是驾驭 App Router 数据层的前提。

缓存总览 ​

┌──────────────────────────────────────────────────────────┐
│                    浏览器 / CDN                           │
│              Cache-Control / ETag / Stale-While-Revalidate│
└───────────────────────────┬──────────────────────────────┘
                            │ (HTTP 响应)
┌───────────────────────────┴──────────────────────────────┐
│  ① Request Memoization   请求级去重                       │
│    作用范围:单次渲染请求 (同一 React 树)                  │
│    生命周期:请求完成即销毁                                │
│    存储位置:React 内存                                    │
│    失效方式:请求结束自动清除                              │
├──────────────────────────────────────────────────────────┤
│  ② Data Cache            数据缓存                         │
│    作用范围:跨请求、跨部署 (可持久化)                     │
│    生命周期:手动清除或过期                                │
│    存储位置:服务端持久化 (文件系统/Redis)                 │
│    失效方式:revalidateTag / revalidatePath / 时间过期     │
├──────────────────────────────────────────────────────────┤
│  ③ Full Route Cache      完整路由缓存                     │
│    作用范围:静态生成的 HTML/RSC Payload                   │
│    生命周期:重新部署或手动清除                            │
│    存储位置:服务端 (持久化)                               │
│    失效方式:Data Cache 失效 或 重新部署                   │
├──────────────────────────────────────────────────────────┤
│  ④ Router Cache          客户端路由缓存                   │
│    作用范围:浏览器内存 (单次会话)                         │
│    生命周期:会话结束 / 时间过期 / 手动清除                │
│    存储位置:浏览器内存                                    │
│    失效方式:revalidatePath / 路由导航 / 时间过期           │
└──────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

层间依赖关系 ​

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│   ① Request Memoization                                    │
│       │  fetch 去重                                          │
│       ▼                                                     │
│   ② Data Cache ─────────────────────┐                      │
│       │  fetch 响应持久化            │ 对不可缓存请求则      │
│       ▼                              │ 穿透 Data Cache       │
│   ③ Full Route Cache ────────────────┘                      │
│       │  HTML + RSC Payload 持久化                           │
│       ▼                                                     │
│   ④ Router Cache (客户端)                                   │
│       │  RSC Payload 缓存 + 预取                             │
│       ▼                                                     │
│    用户可见页面                                              │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

各层详细说明 ​

第1层:Request Memoization (请求去重)

同一渲染请求内,React 的 cache() 包装的 fetch 会自动去重。多个组件调用同一个 URL + 参数的 fetch,只会发出一次网络请求。

tsx
// page.tsx 中的两个组件分别调用同一 API
// 实际只发出一次 fetch 请求
async function UserProfile({ userId }: { userId: string }) {
  const user = await fetchUser(userId) // ← 第1次 "调用"
  return <div>{user.name}</div>
}

async function UserStats({ userId }: { userId: string }) {
  const user = await fetchUser(userId) // ← 同样参数,自动去重
  // 因为 Request Memoization,不会重复请求
  return <div>{user.stats}</div>
}

export default async function Page({ params }: { params: Promise<{ userId: string }> }) {
  const { userId } = await params
  return (
    <>
      <UserProfile userId={userId} />
      <UserStats userId={userId} />
    </>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

关键特征:请求级 memoization 仅在同一 React 渲染树内生效。请求完成后,所有记录被丢弃。它不跨请求,不提供持久化。

第2层:Data Cache (数据缓存)

Data Cache 持久化 fetch 的响应。默认行为(开发环境不同)是 cache: 'force-cache',即响应被缓存到服务端存储。

请求1: fetch('/api/projects') → 服务端执行 → 结果写入 Data Cache
请求2: fetch('/api/projects') → 命中 Data Cache → 直接返回 (不执行)
请求3: fetch('/api/projects') → 命中 Data Cache → 直接返回
...直到缓存过期或被手动清除...
1
2
3
4

Data Cache 的失效方式有三种:

  • 时间窗口:fetch(url, { next: { revalidate: 60 } })
  • 标签清除:revalidateTag('projects')
  • 路径清除:revalidatePath('/dashboard')

第2部分:fetch 缓存策略 ​

四种 fetch 缓存模式 ​

┌─────────────────────────────────────────────────────────────────┐
│  fetch 缓存模式决策树                                            │
│                                                                 │
│  ┌───────┐     ┌──────────────────┐     ┌────────────────────┐  │
│  │ fetch │────▶│ cache: no-store? │──是─▶│ 每次请求都执行      │  │
│  └───────┘     └────────┬─────────┘     │ 不写入 Data Cache   │  │
│                         │ 否             └────────────────────┘  │
│                         ▼                                       │
│                ┌──────────────────┐                             │
│                │ next.revalidate? │─有值─▶ OSW (过期可复用)     │
│                └────────┬─────────┘       │ revalidate 秒内用   │
│                         │ 无              │ 旧缓存,过时后后台   │
│                         ▼                 │ 重新请求             │
│                ┌──────────────────┐       └─────────────────────│
│                │ cache:           │                             │
│                │ force-cache?     │─是─▶ 持久缓存               │
│                └────────┬─────────┘       │ 除非手动失效或部署   │
│                         │ 否              │ 始终返回缓存值       │
│                         ▼                 └─────────────────────│
│                  跟随框架默认行为                               │
│                  (与 force-cache 行为等同)                       │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

cache: 'force-cache' (默认) ​

tsx
// 默认就是 force-cache,显式写出使意图清晰
const projectsRes = await fetch('https://api.example.com/projects', {
  cache: 'force-cache',
})
// 首次请求: 执行网络请求 → 写入 Data Cache
// 后续请求: 命中 Data Cache → 不执行网络请求
// 缓存持续到: revalidateTag('...') / revalidatePath('...') / 重新部署
1
2
3
4
5
6
7

何时使用:数据变化不频繁、对实时性要求高的场景(如产品目录、文章内容、配置数据)。

cache: 'no-store' ​

tsx
// 每次请求都重新获取
const statusRes = await fetch('https://api.example.com/status', {
  cache: 'no-store',
})
// 每次请求: 执行网络请求 → 不写入 Data Cache
// 请求级去重 (Request Memoization) 仍然生效
1
2
3
4
5
6

何时使用:需要实时数据、用户特定数据(包含 cookie/authorization)、高频变化的数据。

注意:no-store 也会导致所在的 Layout / Page 在每次请求时动态渲染,等效于 export const dynamic = 'force-dynamic'。

next.revalidate (ISR 时间窗口) ​

tsx
// 60 秒内"可能"返回旧数据,过时后触发后台重新请求
const res = await fetch('https://api.example.com/dashboard', {
  next: { revalidate: 60 },
})
// 行为 (Stale-While-Revalidate):
//   t=0:    发出请求 → 结果写入缓存
//   t=30:   命中缓存 → 返回缓存值 (新鲜期内)
//   t=65:   缓存过时 → 仍返回旧缓存值 (stale)
//           → 后台触发新请求更新缓存 (revalidate)
//   t=66:   后续请求命中已更新的缓存
1
2
3
4
5
6
7
8
9
10

next.revalidate 是 ISR 的核心机制:低 TTL 获得新鲜度,高 TTL 获得缓存命中率。典型的 TTL 根据业务对陈旧数据的容忍度决定 — 仪表盘可能用 60s,产品目录可能用 3600s。

revalidatePath vs revalidateTag ​

┌─────────────────────────────────────────────────────────────────┐
│  revalidatePath 与 revalidateTag 对比                            │
│                                                                 │
│  revalidatePath('/dashboard')       revalidateTag('projects')    │
│  ┌────────────────────────┐         ┌────────────────────────┐  │
│  │ 按 URL 路径清除        │         │ 按标签清除             │  │
│  │ 影响: 该路径及其子路径 │         │ 影响: 所有标有该标签   │  │
│  │ 的 Full Route Cache    │         │ 的 fetch 结果          │  │
│  │ 和 Data Cache          │         │                        │  │
│  └────────────────────────┘         └────────────────────────┘  │
│                                                                 │
│ 使用场景:                  使用场景:                             │
│  项目详情页面修改后        跨多个页面共享的数据更新             │
│  清除该页面及所有子路由    如: 用户信息、产品列表               │
│                                                                 │
│  标签模式更解耦 — 数据变更时不需要知道谁在消费这些数据           │
│  路径模式更直观 — 适合页面级缓存清除                            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
tsx
// 标签模式 — 推荐用于跨页面共享数据
async function getProjects() {
  const res = await fetch('https://api.example.com/projects', {
    next: { tags: ['projects'] },
  })
  return res.json()
}

// 任何地方修改了项目数据后
async function deleteProject(id: string) {
  'use server'
  await db.project.delete(id)
  // 精确清除
  revalidateTag('projects')        // 项目列表缓存失效
  revalidateTag(`project:${id}`)   // 项目详情缓存失效
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
tsx
// 路径模式 — 适合页面级操作
async function updateProfile(formData: FormData) {
  'use server'
  await db.user.update(/* ... */)
  revalidatePath('/dashboard/profile')   // 清除当前页面
  revalidatePath('/dashboard', 'layout') // 清除布局 (含侧边栏用户信息)
  // 'layout' 参数: 清除该路径及其下所有 Layout
  // 'page' 参数(默认): 仅清除该路径的 Page
}
1
2
3
4
5
6
7
8
9

第3部分:ISR (Incremental Static Regeneration) ​

ISR 让你在构建后按需或按时间窗口重新生成静态页面,兼顾静态站点的性能与动态内容的新鲜度。

SSG vs SSR vs ISR ​

┌─────────────────────────────────────────────────────────────────┐
│                    渲染策略对比矩阵                              │
├────────────┬──────────────┬──────────────┬──────────────────────┤
│            │     SSG      │     SSR      │         ISR          │
│            │ Static Site  │ Server-Side  │ Incremental Static   │
│            │ Generation   │ Rendering    │ Regeneration         │
├────────────┼──────────────┼──────────────┼──────────────────────┤
│ 生成时机   │ 构建时一次   │ 每次请求     │ 首次请求 + 后台更新   │
│ TTFB       │ 极快 (静态)  │ 慢 (实时)    │ 首次可能慢,后续极快  │
│ 新鲜度     │ 低 (至下次   │ 高 (实时)    │ 中 (时间窗口内)      │
│            │  构建)       │              │                      │
│ 服务器负载 │ 极低         │ 高           │ 低 (仅偶尔重新生成)  │
│ 适用场景   │ 文档/博客    │ 实时仪表盘   │ 产品页面/文章/目录   │
│ 缓存位置   │ CDN 边缘     │ 源服务器     │ CDN 边缘 (stale)     │
│ 失效方式   │ 重新部署     │ 无缓存       │ revalidateTag/Path   │
├────────────┴──────────────┴──────────────┴──────────────────────┤
│                                                                 │
│  ISR = SSG 的性能 + 动态内容的更新能力                          │
│  本质: 构建静态 HTML → 缓存 → 后台按规则重新生成                │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

generateStaticParams + dynamicParams ​

tsx
// 生成静态路径
export async function generateStaticParams() {
  const posts = await db.post.findMany({ select: { slug: true } })
  return posts.map((post) => ({ slug: post.slug }))
  // 构建时: 为每个 slug 预生成静态页面
}

// dynamicParams 控制未预生成的路径行为
export const dynamicParams = true  // 默认: 未预生成则动态渲染 (ISR 按需)
// export const dynamicParams = false // 未预生成 → 404

export default async function PostPage({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const post = await getPost(slug)
  return <PostView post={post} />
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

当 dynamicParams = true:

┌───────────────────────────────────────────────────────────┐
│  ISR 按需生成流程                                          │
│                                                           │
│  用户访问 /posts/new-article                              │
│       │                                                   │
│       ▼                                                   │
│  ┌─────────────┐    未预生成    ┌─────────────────────┐  │
│  │ 检查是否已  │──────────────▶│ 动态生成 HTML        │  │
│  │ 预生成?     │               │ (首次请求可能慢)     │  │
│  └─────┬───────┘               └──────────┬──────────┘  │
│        │ 已预生成                          │              │
│        ▼                                  ▼              │
│  ┌─────────────┐               ┌─────────────────────┐  │
│  │ 返回静态    │               │ 写入 Full Route     │  │
│  │ HTML (极快) │               │ Cache              │  │
│  └─────────────┘               └──────────┬──────────┘  │
│                                           │              │
│                                           ▼              │
│                                ┌─────────────────────┐  │
│                                │ 后续请求: 直接返回   │  │
│                                │ 缓存结果 (极快)     │  │
│                                └─────────────────────┘  │
│                                                           │
└───────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

按需重新验证 (On-Demand Revalidation) ​

ISR 最强大的模式不是"等时间到了自动刷新",而是"数据变了主动通知框架重新生成"。

tsx
// 典型的 ISR + 按需重新验证架构
// app/posts/[slug]/page.tsx

export const revalidate = 3600 // 1小时内返回静态缓存 (兜底)

async function getPost(slug: string) {
  const res = await fetch(`https://cms.example.com/posts/${slug}`, {
    next: { tags: [`post:${slug}`] },
  })
  return res.json()
}

export default async function PostPage({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const post = await getPost(slug)
  return <PostView post={post} />
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
tsx
// 在 CMS webhook 或 Server Action 中按需重新验证
// app/api/revalidate/route.ts
export async function POST(request: Request) {
  const { slug, secret } = await request.json()

  // 安全校验
  if (secret !== process.env.REVALIDATION_SECRET) {
    return Response.json({ message: 'Invalid secret' }, { status: 401 })
  }

  // 按需失效,而不是等 3600 秒
  revalidateTag(`post:${slug}`)
  revalidatePath(`/posts/${slug}`)

  return Response.json({ revalidated: true })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

这个组合的意义:revalidate = 3600 是兜底的性能保障(最坏情况读者会看到1小时前的旧内容),而 webhook 触发的 revalidateTag 是在内容发布后秒级更新。

ISR 与 Data Cache 的关系 ​

┌───────────────────────────────────────────────────────────┐
│  ISR 涉及两层缓存的协同                                    │
│                                                           │
│  ┌─────────────────┐                                      │
│  │   Data Cache    │ ← fetch 响应缓存                     │
│  │   (next.tags)   │    revalidateTag 清除此层            │
│  └────────┬────────┘                                      │
│           │ 提供数据                                       │
│           ▼                                               │
│  ┌─────────────────┐                                      │
│  │ Full Route Cache│ ← 页面 HTML + RSC Payload 缓存       │
│  │                 │    Data Cache 失效后,重新渲染页面    │
│  │                 │    时重新写入 Full Route Cache       │
│  └────────┬────────┘                                      │
│           │ Data Cache 不变时返回静态                      │
│           ▼                                               │
│  ┌─────────────────┐                                      │
│  │   Router Cache  │ ← 客户端会话内缓存                   │
│  │   (客户端)      │    路由导航时的即时响应               │
│  └─────────────────┘                                      │
│                                                           │
│  失效链: revalidateTag → Data Cache 失效                  │
│       → Full Route Cache 重新生成                         │
│       → Router Cache 在下次导航时获取新数据                │
│                                                           │
└───────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

第4部分:Server Actions 与数据变更 ​

Server Action 是 Next.js 在 App Router 中引入的服务端变更机制。它不是单纯的 RPC — 它直接在服务端执行,可以读取数据库、调用内部服务、访问环境变量。

'use server' 标记 ​

tsx
// 方式一: 在文件级别标记 (整个文件所有导出函数都是 Server Actions)
'use server'

export async function createProject(formData: FormData) {
  // 此函数在服务端执行
}

export async function deleteProject(id: string) {
  // 此函数也在服务端执行
}

// 方式二: 在函数级别标记 (混合文件)
// app/actions/projects.ts
export async function someClientHelper() {
  // 客户端函数
}

export async function createProject(formData: FormData) {
  'use server'
  // 只有此函数在服务端执行
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

建议:将 Server Actions 放在独立文件中(如 app/actions/ 或 server/mutations/),与客户端逻辑严格分离。

Server Action 的序列化限制 ​

Server Actions 通过网络发送给服务端,因此参数必须可序列化:

┌───────────────────────────────────────────────────────────┐
│  Server Action 可序列化类型清单                            │
│                                                           │
│  ✅ 可序列化:                                              │
│     • 原始值: string, number, boolean, null, undefined    │
│     • 可迭代对象: Array, Map, Set                         │
│     • 特殊对象: Date, FormData, File                      │
│     • 普通对象 (无方法、无 class 实例)                     │
│     • Promise (作为返回值)                                 │
│                                                           │
│  ❌ 不可序列化:                                            │
│     • 函数 / 闭包                                         │
│     • Class 实例 (除内置类型外)                            │
│     • DOM 元素 / React 元素                                │
│     • Symbol                                              │
│     • WeakMap / WeakSet                                   │
│     • 超过 1MB 的参数 (服务端限制)                         │
│                                                           │
│  ⚠️  注意: TypeScript 类型在运行时不存在,不能阻止        │
│     不可序列化参数的传递。需要运行时校验。                  │
│                                                           │
└───────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

Server Action 授权模型 ​

Server Action 在服务端执行,但它接收的输入来自不可信的客户端。必须逐层校验:

tsx
'use server'

import { z } from 'zod'

// 1. 定义运行时校验 schema
const RenameProjectSchema = z.object({
  projectId: z.string().uuid(),
  name: z.string().min(1).max(100),
})

export async function renameProject(formData: FormData) {
  // 2. 认证 — 验证请求者身份
  const session = await requireSession()
  if (!session) {
    throw new AuthError('Unauthorized')
  }

  // 3. 运行时输入校验 — 不信任任何客户端数据
  const input = RenameProjectSchema.parse({
    projectId: formData.get('projectId'),
    name: formData.get('name'),
  })
  // 此时 input 已被净化并类型安全

  // 4. 对象级授权 — 验证该用户是否有权操作此资源
  const canEdit = await assertCanEditProject(session.userId, input.projectId)
  if (!canEdit) {
    throw new ForbiddenError(`Cannot edit project ${input.projectId}`)
  }
  // 返回 404 而非 403 以隐藏资源存在性 (安全考虑)

  // 5. 执行业务逻辑
  await db.project.update({
    where: { id: input.projectId },
    data: { name: input.name },
  })

  // 6. 触发缓存失效
  revalidateTag(`project:${input.projectId}`)
  revalidateTag('projects')

  // 7. 审计日志 (服务端)
  await auditLog.create({
    action: 'project.rename',
    userId: session.userId,
    resourceId: input.projectId,
    metadata: { oldName: /* ... */, newName: input.name },
  })

  return { success: true }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

useActionState 与错误处理 ​

tsx
'use client'

import { useActionState } from 'react'
import { renameProject } from '@/server/mutations/project'

const initialState = { message: '', errors: {} as Record<string, string> }

export function RenameForm({ projectId, currentName }: { projectId: string; currentName: string }) {
  // useActionState 管理 Server Action 的执行状态
  const [state, formAction, isPending] = useActionState(
    renameProject,
    initialState,
  )

  return (
    <form action={formAction}>
      <input type="hidden" name="projectId" value={projectId} />
      <input
        name="name"
        defaultValue={currentName}
        aria-describedby="name-error"
      />
      {state.errors?.name && (
        <p id="name-error" role="alert">{state.errors.name}</p>
      )}
      <button type="submit" disabled={isPending}>
        {isPending ? 'Renaming...' : 'Rename'}
      </button>
      {state.message && (
        <p role="status">{state.message}</p>
      )}
    </form>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

Server Action 内部使用 revalidatePath / revalidateTag ​

tsx
'use server'

export async function deleteComment(commentId: string, postSlug: string) {
  await db.comment.delete({ where: { id: commentId } })

  // 标签清除 — 跨页面数据
  revalidateTag(`post:${postSlug}`)
  revalidateTag('comments')

  // 路径清除 — 页面级缓存
  revalidatePath(`/posts/${postSlug}`)
  // 如果评论还在侧边栏 Layout 中
  revalidatePath('/posts', 'layout')
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

关键规则:revalidatePath 和 revalidateTag 仅在服务端执行时生效 — 也就是说,它们只能在 Server Action、Route Handler 或服务端组件中调用。


第5部分:乐观更新 (Optimistic Updates) ​

乐观更新在服务端响应前就将 UI 更新为用户期望的最终状态,使应用"感觉瞬间完成"。React 19 通过 useOptimistic hook 提供了声明式 API。

useOptimistic hook ​

tsx
'use client'

import { useOptimistic, useState } from 'react'
import { toggleTodo } from '@/server/actions'

interface Todo {
  id: string
  title: string
  completed: boolean
}

export function TodoList({ initialTodos }: { initialTodos: Todo[] }) {
  // useOptimistic(当前状态, reducer)
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    initialTodos,
    (state: Todo[], updatedTodo: Todo) =>
      state.map((todo) =>
        todo.id === updatedTodo.id ? updatedTodo : todo,
      ),
  )

  async function handleToggle(todo: Todo) {
    // 1. 乐观更新 — UI 立即反应
    addOptimisticTodo({ ...todo, completed: !todo.completed })

    // 2. 发送 Server Action
    await toggleTodo(todo.id, !todo.completed)
    // 3. 如果失败,React 自动回滚 optimisticTodos
  }

  return (
    <ul>
      {optimisticTodos.map((todo) => (
        <li key={todo.id}>
          <button onClick={() => handleToggle(todo)}>
            {todo.completed ? '✅' : '⬜'} {todo.title}
          </button>
        </li>
      ))}
    </ul>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

Server Action + 乐观更新完整流程 ​

┌───────────────────────────────────────────────────────────┐
│  乐观更新时序图                                            │
│                                                           │
│  用户操作   客户端                       服务端            │
│  ────────  ────────────────────────────  ───────────────  │
│     │                                    │                │
│     │ 点击"完成"按钮                      │                │
│     │────▶ ① 立即渲染乐观状态             │                │
│     │     (勾选图标立即变化)              │                │
│     │                                    │                │
│     │     ② 发送 Server Action ────────▶ ③ 执行变更     │
│     │                                    │   更新数据库    │
│     │                                    │               │
│     │     ④ 响应返回 ◀───────────────── ⑤ revalidate    │
│     │     │                              │               │
│     │     ├─ 成功: 乐观状态已成真实状态  │               │
│     │     │   React 不再干预             │               │
│     │     │                              │               │
│     │     └─ 失败: React 自动回滚        │               │
│     │         乐观状态恢复到变更前       │               │
│     │         显示错误提示               │               │
│     │                                    │               │
└───────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

错误回滚策略 ​

tsx
'use client'

import { useOptimistic, useState } from 'react'

export function TodoListWithRollback({ initialTodos }: { initialTodos: Todo[] }) {
  const [error, setError] = useState<string | null>(null)
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    initialTodos,
    (state, updatedTodo: Todo) =>
      state.map((t) => (t.id === updatedTodo.id ? updatedTodo : t)),
  )

  async function handleToggle(todo: Todo) {
    setError(null)
    const optimisticTodo = { ...todo, completed: !todo.completed }

    // 乐观更新
    addOptimisticTodo(optimisticTodo)

    try {
      await toggleTodo(todo.id, !todo.completed)
    } catch (err) {
      // useOptimistic 自动回滚
      // 但还需要告知用户
      setError(
        err instanceof Error
          ? err.message
          : 'Failed to update. Please try again.',
      )
      // 可选的重新同步
      // router.refresh()  // 强制从服务端获取最新数据
    }
  }

  return (
    <>
      {error && <div role="alert" className="error">{error}</div>}
      <ul>
        {optimisticTodos.map((todo) => (
          <li key={todo.id}>
            <button onClick={() => handleToggle(todo)}>
              {todo.completed ? '✅' : '⬜'} {todo.title}
            </button>
          </li>
        ))}
      </ul>
    </>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

乐观更新的适用判断 ​

┌───────────────────────────────────────────────────────────┐
│  乐观更新适用性判断                                        │
│                                                           │
│  ✅ 适合乐观更新:                                          │
│     • 失败概率极低 (如内部服务调用)                        │
│     • 回滚成本低 (UI 状态简单)                             │
│     • 用户体验显著提升 (频繁点击的场景)                    │
│     • 示例: Toggle 开关、重命名、拖拽排序、收藏/点赞       │
│                                                           │
│  ⚠️  谨慎使用:                                             │
│     • 失败率中等 (< 5%)                                    │
│     • 需要明确的回滚提示                                   │
│     • 示例: 表单提交、评论发布                             │
│                                                           │
│  ❌ 不应乐观更新:                                          │
│     • 失败率高 (> 5%)                                      │
│     • 幂等性无法保证                                       │
│     • 回滚成本高或不可逆                                   │
│     • 涉及资金、库存、权限                                 │
│     • 需要服务端确认的最终状态 (如支付结果)                │
│     • 示例: 支付、退款、库存扣减、权限变更、删除操作       │
│                                                           │
│  黄金法则: 如果客户端不能安全地"猜测"最终状态,            │
│            就不要使用乐观更新。                             │
│                                                           │
└───────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

乐观更新 + 重新验证 ​

tsx
async function handleRename(projectId: string, newName: string) {
  // 乐观更新
  addOptimisticProject({ id: projectId, name: newName })

  try {
    await renameProject(projectId, newName)
    // Server Action 内部已经调用了 revalidateTag
    // 但客户端 Router Cache 仍持有旧数据
    // 使用 router.refresh() 强制刷新当前路由
    refresh() // from useRouter
    // 或者依赖 Server Action 内部的 revalidatePath
    // 触发客户端自动更新
  } catch {
    // useOptimistic 自动回滚
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

第6部分:React cache() 与 unstable_cache ​

Next.js 的缓存生态中,React.cache() 和 next/cache 的 unstable_cache 是两个层级不同的工具,分别作用于请求级去重和跨请求持久化缓存。

React cache() — 请求级去重 ​

tsx
import { cache } from 'react'

// cache() 包装的函数在同一渲染请求中只会执行一次
export const getUser = cache(async (userId: string) => {
  console.log('Fetching user:', userId) // 同一 userId 只打印一次
  const user = await db.user.findUnique({ where: { id: userId } })
  return user
})

// ── 使用场景 ──
// Page 渲染:
//   <UserProfile userId="1" />  → getUser("1") → 实际执行 DB 查询
//   <UserSettings userId="1" /> → getUser("1") → 命中缓存, 不再执行
//   <UserAvatar userId="1" />   → getUser("1") → 命中缓存, 不再执行
// 三次"调用",一次真实执行。
//
// 请求结束后,缓存销毁。下一个请求重新计数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

cache() 与 fetch 去重的区别:

┌───────────────────────────────────────────────────────────┐
│  React cache() vs fetch 自动去重                           │
│                                                           │
│  React cache()            │  fetch 自动去重                │
│  ─────────────────────────│────────────────────────────── │
│  包装任意异步函数          │  仅对原生 fetch 生效          │
│  包括 ORM、数据库查询     │  不包括 fetch 封装层           │
│  需要显式包装             │  自动启用                      │
│  整个渲染树共享           │  整个渲染树共享                │
│                                                           │
│  典型场景:                 │  典型场景:                     │
│    db.user.findUnique()   │  fetch('/api/data')           │
│    prisma.user.findFirst  │  fetch(thirdPartyUrl)         │
│    ORM 查询去重           │  外部 API 请求去重             │
│                                                           │
│  关键区别: cache() 是通用去重,不限于 fetch               │
│                                                           │
└───────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
tsx
// cache() 的最佳实践: 组合使用
import { cache } from 'react'

// 基础数据函数 (带 cache)
const getProject = cache(async (id: string) => {
  const project = await db.project.findUnique({ where: { id } })
  if (!project) notFound()
  return project
})

const getProjectMembers = cache(async (projectId: string) => {
  return db.projectMember.findMany({ where: { projectId } })
})

// 授权检查 (带 cache,避免重复验证)
const getAuthCheck = cache(async (userId: string, projectId: string) => {
  const member = await db.projectMember.findFirst({
    where: { userId, projectId },
  })
  return { canEdit: member?.role === 'admin' || member?.role === 'editor' }
})

// 页面中使用
export default async function ProjectPage({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params

  // 并行获取 (cache 确保去重)
  const [project, members, auth] = await Promise.all([
    getProject(id),
    getProjectMembers(id),
    getAuthCheck(session.userId, id),
  ])
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

unstable_cache — 持久化缓存 ​

tsx
import { unstable_cache } from 'next/cache'

// unstable_cache(fn, keyParts, options)
export const getProjectStats = unstable_cache(
  async (projectId: string) => {
    // 此函数的结果跨请求持久化缓存
    const stats = await db.$queryRaw`/* 复杂统计查询 */`
    return stats
  },
  ['project-stats'],       // 缓存键的前缀
  {
    revalidate: 3600,      // 1小时后过期 (ISR 时间窗口)
    tags: ['project-stats'], // 可通过 revalidateTag 手动失效
  },
)

// 使用
const stats = await getProjectStats(projectId)
// 首次: 执行查询 → 写入 Data Cache
// 后续: 命中 Data Cache → 返回 (与 fetch force-cache 行为一致)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

三者关系图 ​

┌───────────────────────────────────────────────────────────┐
│  cache / unstable_cache / Data Cache 关系                  │
│                                                           │
│  ┌─────────────────────────────────────────────────────┐  │
│  │              ① Request Memoization                  │  │
│  │  React.cache()  + fetch 自动去重                    │  │
│  │  作用: 同一渲染树内的重复调用去重                   │  │
│  │  生命周期: 请求结束即释放                            │  │
│  └──────────────────────┬──────────────────────────────┘  │
│                         │ cache MISS (首次) / 不涉及缓存   │
│                         ▼                                 │
│  ┌─────────────────────────────────────────────────────┐  │
│  │              ② Data Cache                           │  │
│  │  unstable_cache()  + fetch 响应持久化               │  │
│  │  作用: 跨请求复用数据                                │  │
│  │  生命周期: 手动清除或过期                            │  │
│  └──────────────────────┬──────────────────────────────┘  │
│                         │ Data Cache 变更                  │
│                         ▼                                 │
│  ┌─────────────────────────────────────────────────────┐  │
│  │              ③ Full Route Cache                     │  │
│  │  页面 HTML + RSC Payload 持久化                     │  │
│  │  作用: 跳过渲染直接返回静态产物                     │  │
│  └─────────────────────────────────────────────────────┘  │
│                                                           │
│  unstable_cache 本质上是 Data Cache 的非 fetch API         │
│  它将任意函数的返回值以相同方式持久化到 Data Cache         │
│                                                           │
└───────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

unstable_cache 键策略 ​

tsx
// ⚠️ 错误: 缓存键不包含变化参数
export const getUserProjects = unstable_cache(
  async (userId: string) => {
    return db.project.findMany({ where: { ownerId: userId } })
  },
  ['user-projects'], // ← 缺少 userId!
  { tags: ['projects'] },
)
// 问题: 不同 userId 会命中同一缓存 → 返回错误用户的数据

// ✅ 正确: 缓存键包含所有业务相关的变化参数
export const getUserProjects = unstable_cache(
  async (userId: string) => {
    return db.project.findMany({ where: { ownerId: userId } })
  },
  ['user-projects'], // 前缀
  {
    // 动态部分通过函数参数自动加入缓存键
    tags: ['projects', 'user-projects'],
  },
)
// unstable_cache 自动将函数参数哈希后加入缓存键
// 不同 userId → 不同缓存键 → 不会错误共享
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

cacheTag() — 更精细的标签控制 ​

tsx
import { unstable_cache, cacheTag } from 'next/cache'

export const getDashboardData = unstable_cache(
  async (orgId: string) => {
    // cacheTag() 可以在函数体内动态决定标签
    cacheTag('dashboard')
    cacheTag(`org:${orgId}`)
    cacheTag(`dashboard:org:${orgId}`)

    const [projects, members, activity] = await Promise.all([
      db.project.findMany({ where: { orgId } }),
      db.member.findMany({ where: { orgId } }),
      db.activity.findMany({ where: { orgId }, take: 20 }),
    ])

    return { projects, members, activity }
  },
  ['dashboard'],
  { revalidate: 300 },
)

// 清除时,可以精确控制影响范围
revalidateTag(`dashboard:org:${orgId}`) // 只清除该组织的仪表盘
revalidateTag('dashboard')               // 清除所有组织的仪表盘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

第7部分:常见陷阱 ​

陷阱 1:缓存过度导致数据陈旧 ​

tsx
// ❌ 问题:缓存时间过长,用户看到过时的数据
const res = await fetch('https://api.example.com/stock-price', {
  cache: 'force-cache', // 默认缓存,手动失效前永远不会更新
})
1
2
3
4

解决方案:为不同数据选择合理的缓存策略。

tsx
// 实时数据 — 不缓存
const stockPrice = await fetch(apiUrl, { cache: 'no-store' })

// 准实时数据 — 短 TTL
const dashboard = await fetch(apiUrl, { next: { revalidate: 60 } })

// 相对稳定的数据 — 长 TTL + 按需失效
const articles = await fetch(apiUrl, {
  next: { revalidate: 3600, tags: ['articles'] },
})

// 静态数据 — 构建时生成
const config = await fetch(apiUrl) // SSG: 构建时执行
1
2
3
4
5
6
7
8
9
10
11
12
13

陷阱 2:noStore 误用导致性能下降 ​

tsx
import { unstable_noStore as noStore } from 'next/cache'

// ❌ 整个页面动态渲染 — 即使只有一小部分需要实时数据
export default async function Page() {
  noStore() // 整个页面的所有 fetch 都绕过缓存
  const projects = await getProjects()    // 本可以缓存
  const settings = await getSettings()    // 本可以缓存
  const alerts = await getAlerts()         // 需要实时
  return <Dashboard projects={projects} settings={settings} alerts={alerts} />
}
1
2
3
4
5
6
7
8
9
10

解决方案:将实时数据拆分到独立组件,用 Suspense 包裹。

tsx
// ✅ 只让实时部分动态渲染
export default function Page() {
  return (
    <div>
      <Suspense fallback={<ProjectsSkeleton />}>
        <CachedProjectList />      {/* 缓存 render */}
      </Suspense>
      <Suspense fallback={<AlertsSkeleton />}>
        <RealtimeAlerts />         {/* 动态 render */}
      </Suspense>
    </div>
  )
}

async function RealtimeAlerts() {
  noStore() // 只影响此组件内的 fetch
  const alerts = await getAlerts()
  return <AlertsView alerts={alerts} />
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

陷阱 3:Router Cache 的客户端缓存行为 ​

┌───────────────────────────────────────────────────────────┐
│  Router Cache 行为关键点                                   │
│                                                           │
│  行为:                                                     │
│  • 前进/后退导航 → 立即返回缓存页面 (包括布局)             │
│  • Page 级别缓存默认 30 秒                                 │
│  • Layout 级别缓存在导航间保持 (不重新渲染)                │
│  • 预取标签在视口时自动预热缓存                            │
│                                                           │
│  常见问题:                                                 │
│  ┌─────────────────────────────────────────────────────┐  │
│  │ 用户在 /projects/1 重命名项目                       │  │
│  │ → Server Action 成功 + revalidatePath               │  │
│  │ → 服务端 Full Route Cache 已清除                    │  │
│  │ → 但浏览器 Router Cache 仍返回旧页面!               │  │
│  │ → 用户需要手动刷新或等待 30 秒                      │  │
│  └─────────────────────────────────────────────────────┘  │
│                                                           │
│  解决方案:                                                 │
│  router.refresh()  — 强制刷新当前路由的所有服务端组件     │
│  revalidatePath()  — 也会使 Router Cache 失效             │
│  配置 staleTimes    — 实验性 API,控制缓存过期时间         │
│                                                           │
└───────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
tsx
// 在 Server Action 调用后手动刷新客户端缓存
'use client'

import { useRouter } from 'next/navigation'

export function RenameForm({ projectId }: { projectId: string }) {
  const router = useRouter()

  async function handleSubmit(formData: FormData) {
    await renameProject(formData) // Server Action
    router.refresh()              // 强制刷新当前路由
    // 现在 Router Cache 更新了,用户看到最新数据
  }

  return (
    <form action={handleSubmit}>
      {/* ... */}
    </form>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

陷阱 4:fetch 自动去重与 GraphQL 客户端 ​

Next.js 的 fetch 自动去重只对原生 fetch 生效。如果你使用 Apollo Client、urql 或任何封装了 fetch 的库,请求去重不会自动发生。

tsx
// ❌ Apollo Client 请求不会自动去重
const { data: user1 } = await client.query({ query: USER_QUERY, variables: { id: '1' } })
const { data: user2 } = await client.query({ query: USER_QUERY, variables: { id: '1' } })
// 两次真实网络请求!

// ✅ 手动使用 React cache() 包装
import { cache } from 'react'

export const getCachedUser = cache(async (userId: string) => {
  const { data } = await client.query({
    query: USER_QUERY,
    variables: { id: userId },
  })
  return data.user
})

// 现在同一渲染树内只会执行一次
const userA = await getCachedUser('1')
const userB = await getCachedUser('1') // 命中 cache
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

陷阱 5:在 Server Action 中忘记重新验证 ​

tsx
// ❌ 数据已变更但缓存未失效 — 用户看不到新数据
'use server'
export async function addComment(postId: string, content: string) {
  await db.comment.create({ data: { postId, content } })
  // 没有 revalidateTag 或 revalidatePath
  // → Data Cache 和 Full Route Cache 仍返回旧数据
  // → 用户提交评论后看不到自己的评论
}

// ✅ 变更后立即失效相关缓存
'use server'
export async function addComment(postId: string, content: string) {
  await db.comment.create({ data: { postId, content } })
  revalidateTag(`post:${postId}`)
  revalidateTag('comments')
  revalidatePath(`/posts/${postId}`)
  // 缓存失效 → 下次请求重新获取 → 用户看到新评论
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

陷阱 6:开发环境与生产环境缓存行为不一致 ​

Next.js 在开发模式下缓存行为与生产不同:

特性开发模式 (next dev)生产模式 (next start)
fetch 缓存默认 no-store默认 force-cache
Full Route Cache不使用使用
ISR revalidate不生效生效
Router Cache不使用使用 (30s page)

测试缓存策略时必须使用 next build && next start。

陷阱 7:敏感数据泄露到客户端缓存 ​

tsx
// ❌ 包含敏感数据的 fetch 被缓存
const res = await fetch('https://api.example.com/user', {
  headers: { Authorization: `Bearer ${token}` }, // token 随响应被缓存
  // cache: 'force-cache' (默认)
})
// Data Cache 中存储了带 Authorization header 的响应
// 不同用户可能命中同一缓存 → 数据泄露
1
2
3
4
5
6
7

解决方案:

tsx
// 方案 1: 使用 no-store
const res = await fetch('https://api.example.com/user', {
  headers: { Authorization: `Bearer ${token}` },
  cache: 'no-store', // 不缓存用户特定数据
})

// 方案 2: 使用 cookies() 自动分段
// Next.js 在 fetch 中读取 cookies 时,自动使用不同的缓存键

// 方案 3: 在内部函数中获取用户数据,避免通过 fetch 传输 token
async function getUser() {
  'use server'
  const session = await getSession() // 服务端直接读取
  return db.user.findUnique({ where: { id: session.userId } })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

核心总结 ​

Next.js 的数据获取与缓存本质上是四层体系的分工协作:

  1. Request Memoization 保证同一请求内不重复工作
  2. Data Cache 保证跨请求复用数据,由标签和路径控制失效
  3. Full Route Cache 保证静态页面以最快速度返回,由 Data Cache 失效驱动重新生成
  4. Router Cache 保证客户端导航瞬间完成,但需注意陈旧问题

Server Action 提供了服务端变更的安全入口,但必须在每个 Action 内部完成认证、授权、输入校验和缓存失效。乐观更新通过 useOptimistic 提升感知性能,但仅适用于失败率低、回滚成本小的场景。

理解每层缓存在何时被写入、何时被读取、何时被失效,是避免缓存相关 Bug 的关键。


📝 章节测试 ​

1. Next.js 的四层缓存体系中,哪一层的生命周期仅限于单次渲染请求?

A. Data Cache B. Full Route Cache C. Request Memoization D. Router Cache

2. 以下哪个 fetch 配置会让页面在每次请求时动态渲染?

A. { cache: 'force-cache' } B. { next: { revalidate: 60 } } C. { cache: 'no-store' } D. { next: { tags: ['data'] } }

3. ISR 中 generateStaticParams 的作用是什么?

A. 动态生成页面标题 B. 在构建时预生成特定路径的静态页面 C. 在每次请求时动态生成参数 D. 禁用静态生成

4. Server Action 中需要完成哪些安全校验?(多选)

A. 认证(验证用户身份) B. 对象级授权(验证操作权限) C. 运行时输入校验 D. 仅依赖 TypeScript 类型检查

5. 以下哪些场景不适合使用乐观更新?

A. 重命名项目标题 B. 支付订单 C. 切换待办事项完成状态 D. 删除用户账户

6. React cache() 与 unstable_cache 的主要区别是什么?

A. cache() 是服务端持久缓存,unstable_cache 是请求级去重 B. cache() 是请求级去重,unstable_cache 是持久化到 Data Cache C. 两者完全相同,可以互换 D. cache() 只在浏览器端生效

7. 开发模式与生产模式下缓存行为的差异,正确的是?

A. 完全一致,无差异 B. 开发模式默认 cache: 'no-store',生产模式默认 cache: 'force-cache' C. 开发模式缓存时间更长 D. 生产模式不使用任何缓存


📝 参考答案 ​

1. C — Request Memoization 在每次请求结束后自动销毁,仅在同一 React 渲染树内生效。

2. C — cache: 'no-store' 会使所在页面每次请求时动态渲染(等效于 dynamic = 'force-dynamic')。force-cache 使用持久缓存;next.revalidate 使用 ISR 时间窗口;tags 不改变缓存策略,只是增加失效标签。

3. B — generateStaticParams 在构建时生成特定路径的静态页面。未在返回列表中的路径由 dynamicParams 决定行为(动态生成或 404)。

4. A, B, C — Server Action 中必须进行认证(验证身份)、对象级授权(验证权限)和运行时输入校验(Zod 等)。不能仅依赖 TypeScript 类型,因为类型在运行时不存在。

5. B, D — 支付和删除用户账户是高风险操作,不适合乐观更新。重命名和切换待办事项是低风险操作,适合乐观更新提升体验。

6. B — cache() 是 React 提供的请求级去重(同一渲染树内复用),unstable_cache 是 Next.js 提供的持久化缓存(写入 Data Cache,跨请求复用)。

7. B — 开发模式下为方便调试,默认 cache: 'no-store' 不走缓存;生产模式默认 cache: 'force-cache' 使用持久缓存。


📚 相关笔记 ​

  • [[05-nextjs-app-router-and-rendering]] — App Router 路由、服务端/客户端组件与流式渲染
  • [[07-tanstack-query-server-state]] — TanStack Query 客户端服务端状态管理
  • [[04-redux-zustand-state-management]] — Redux 与 Zustand 状态管理
  • [[01-react-components-and-rendering]] — React 组件与渲染基础

🔗 下一步学习 ​

完成本章后,建议继续学习 [[07-tanstack-query-server-state]],理解客户端缓存(TanStack Query)与服务端缓存(Next.js Data Cache)如何互补——前者管理用户会话内的远程状态,后者跨用户复用静态数据。


学习状态:🟡 开始学习

最后更新于:

Pager
上一篇6. Next.js App Router:路由、渲染与工程边界 / Next.js App Router, Routing, Rendering, and Engineering Boundaries
下一篇8. TanStack Query 与服务端状态管理 / TanStack Query and Server State Management

持续记录,持续成长

Copyright © Tidenflow