N08第 1 / 1 课时未开始内容版本 1.0.0 · 核验于 2026-10-26

数据缓存与失效

数据缓存与失效 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

缓存是「少干活」,失效是「该新就新」。读完你应该能说清 Next.js 16 的默认缓存规则,用 revalidatePath / revalidateTag 在数据变化时精准失效,让笔记和进度界面不再展示旧记录。

问题场景

课程页顶部显示「已学完 3 节」。你在笔记页写完一节,回到课程页——数字还是 3。改了内容、更新了进度,界面却躺在旧记录上,这就是缓存失效没做对。

核心解释

先对齐 Next.js 16 的默认规则,两层数据两种命运:

服务端数据层:fetch 默认不缓存。 现代 Next.js 里 fetch 请求默认每次执行都拿最新数据(早期的默认缓存已被移除)。如果你的数据来自数据库直查(如 N07 的 node:sqlite),它本来就不经过 fetch 缓存。真正需要留意的是页面级缓存:没有任何动态信号的页面会在 build 时被预渲染成静态 HTML,之后每次请求都发同一份——数据变了页面不换。给路由段声明重新验证节奏可以开启 ISR:

// app/courses/page.tsx
export const revalidate = 60; // 每 60 秒允许重新生成一次

export default async function CoursesPage() {
  const courses = await listCourses();
  return <CourseList courses={courses} />;
}

客户端 Router Cache:导航时可能复用旧渲染。 为了导航流畅,Next.js 会在浏览器里缓存访问过的路由渲染结果,预取的静态段默认有 5 分钟复用窗口。这就是「改完数据、点回上一页看到旧内容」的第二个来源。

失效的手段是在写操作里主动声明「哪些缓存作废」——把失效动作和写动作绑在一起,这是 Server Actions 的标准姿势:

// app/actions.ts
"use server";
import { revalidateTag } from "next/cache";

export async function completeLesson(lessonId: string) {
  await markLessonDone(lessonId);          // 写库
  revalidateTag("progress");               // 失效所有打了这个标签的缓存
}
// 数据读取处打标签
const progress = await getProgress(userId, { next: { tags: ["progress"] } });

revalidatePath("/courses") 则按路径失效,适合不想维护标签体系的小项目。两者的区别:tag 是「按数据种类失效」(进度变了,所有展示进度的页面一起失效),path 是「按页面失效」。写操作所在的当前页面,Action 完成后会自动带回新的渲染结果,界面立即更新——需要你手动失效的是其他引用了这份数据的页面和 Router Cache 里的旧渲染。

示例拆解

把「改进度 → 看到新数字」的链路串起来:

  • 课程详情页读进度时打上 tags: ["progress"],页面声明 revalidate = 60;
  • 「标记完成」按钮触发 Server Action:写库 → revalidateTag("progress");
  • 验证一:点击后当前页的进度数字立刻 +1(Action 响应自动带回新渲染);
  • 验证二:导航到列表页(预取过的旧渲染),再进入详情页,数字正确——tag 失效把 Router Cache 里的旧渲染一并作废了;
  • 验证三:把 revalidateTag 那行注释掉再改进度,重走一遍,观察旧数字复活——这就是失效缺失的现场。

动手任务

在项目里完成三个小改动:

  1. 给课程页声明 revalidate = 60,在构建输出里确认它变成 ISR 路由(build 输出里显示 Revalidate 标记)。
  2. 实现「标记完成」Server Action:写库 + revalidateTag("progress"),读端打上同名标签。
  3. 故意注释掉 revalidateTag 复现旧数据问题,再用 revalidatePath 替换标签方案修复,对比两者的失效范围。

常见错误

  • 只在读端缓存、忘了在写端失效:缓存窗口期内用户看到旧数据;失效动作必须和写操作绑在同一个事务性质的函数里。
  • revalidatePath 用了不匹配的路径:动态路由要用 /courses/[slug] 或具体路径匹配,路径写错等于没失效。
  • 以为 revalidateTag 会立即重新拉数据:它是「作废」,下次访问才重建;不要指望调用它后同一请求周期里立刻拿到新值。
  • 在客户端组件里直接调 revalidatePath:它是服务端 API,只能出现在 Server Action 或服务端代码里;客户端只负责触发 Action。

检查与复盘

完成下方检查题确认理解。自查清单:

  • 说得清两层数据的去向:服务端 fetch 默认不缓存、Router Cache 在客户端复用
  • 每个写操作都绑定了对应的失效调用,tag 与 path 的选择有理由
  • 验证过「改数据 → 当前页新 → 其他页也新」的完整链路

参考资料

检查题共 2 题,提交后立即显示解析
答案由服务端判定;答错的题会自动进入复习计划。
Q1单选中等

在页面文件里写 export const dynamic = 'force-dynamic',它的作用是?

Q2读代码较难

阅读三个页面。npm run build 之后,多次请求 /a/stats、/b/intro、/c/live,渲染行为正确的是?

// app/a/stats/page.tsx
import { cookies } from 'next/headers';
export default async function A() {
  const c = await cookies();
  return <p>主题:{c.get('theme')?.value ?? '默认'}</p>;
}

// app/b/intro/page.tsx
export default function B() {
  return <p>静态介绍页</p>;
}

// app/c/live/page.tsx
export const dynamic = 'force-dynamic';
export default function C() {
  return <p>实时数据</p>;
}

读完这一课了?

标记完成后,工作台会安排接下来的学习。

Command Palette

Search for a command to run...