On-demand revalidation 是把”静态 + ISR”用出 CMS 手感的关键功能。编辑在 Sanity 里点发布、或者推一个 Markdown commit,webhook 打到 Next.js 应用上,受影响的页面几秒内刷新,不用重新部署,也不用全量重建。用过之后,再为一个错字跑全量部署就觉得很离谱。本文按 Next.js 16(2025 年 10 月发布,下面这些缓存 API 在 16.2 转正,截至 2026 年 6 月为当前版本)把这一套接对。
一句话总结
- 加一条
POST /api/revalidate路由,用 header 密钥保护,按 webhook 传来的内容调revalidatePath或revalidateTag。 - 单篇用
revalidatePath('/zh/articles/[slug]');跨页变化(新增一篇要顺带刷列表页和首页)用revalidateTag('articles', 'max')。 - 在 webhook(route handler)里只能用
revalidateTag/revalidatePath;updateTag只能在 Server Action 里用,专治站内发布要”立刻看到自己刚写的”那种场景。 - 用三条
curl验证:带密钥通过、不带密钥被拒、页面内容确实变了。
背景:四个原语
Next.js 16 的 next/cache 暴露四个失效函数。截至 2026 年 6 月(Next.js 16.2),use cache 这套缓存 API 已转正,unstable_ 前缀都去掉了。
| 函数 | 在哪能用 | 作用范围 | 行为 |
|---|---|---|---|
revalidatePath(path, type?) | Server Function + Route Handler | 单个 page/layout 路径 | 标记该路径为过期,下次访问时刷新 |
revalidateTag(tag, profile?) | Server Function + Route Handler | 带该 tag 的所有缓存项 | profile: 'max' = stale-while-revalidate;不传 profile 走旧的立即过期行为 |
updateTag(tag) | 仅 Server Action | 带该 tag 的所有缓存项 | 立即过期,下次请求等新数据(read-your-own-writes) |
cacheTag(tag) | 'use cache' 函数内部 | 给该缓存项打 tag | 声明上面两个要失效的 tag |
CMS webhook 是外部触发,所以它落在 route handler 里,用 revalidatePath / revalidateTag。updateTag 在这里调不了,会抛 updateTag can only be called from within a Server Action。宿主需要持久化函数缓存:Vercel 自带;Next.js 16.2 的稳定 Adapter API 让 Netlify 和 Cloudflare(走 OpenNext 适配器)也能支持按需 revalidation。
怎么判断你需要它
- 编辑抱怨更新要 15 分钟才能看到(你的完整部署时长)。
- 每页都写了
export const revalidate = 60,CDN 命中率却很差。 - CMS 里有个”发布”按钮的 webhook,但根本没做事。
- 5000 篇文章的站,改一篇就重新构建全部,build minutes 烧光。
快速结论
任何有 CMS 或定期更新的内容站,都该接一条带共享密钥的 on-demand revalidation route handler。单篇用 revalidatePath,跨页变化(新增文章触发首页刷新)用 revalidateTag。
接 route handler
App Router 下最小可用的 /api/revalidate:
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export async function POST(req: NextRequest) {
const secret = req.headers.get('x-revalidate-secret');
if (secret !== process.env.REVALIDATE_SECRET) {
return NextResponse.json({ ok: false, error: 'unauthorized' }, { status: 401 });
}
const body = await req.json().catch(() => ({}));
const { slug, tag, lang } = body as { slug?: string; tag?: string; lang?: string };
if (tag) {
// 'max' 走 stale-while-revalidate:新页面后台渲染时,
// 访客继续看到旧页面,不会卡住。
revalidateTag(tag, 'max');
return NextResponse.json({ ok: true, revalidated: { tag } });
}
if (slug) {
const path = `/${lang ?? 'en'}/articles/${slug}`;
revalidatePath(path);
return NextResponse.json({ ok: true, revalidated: { path } });
}
return NextResponse.json({ ok: false, error: 'missing slug or tag' }, { status: 400 });
}
四个要点:
NextRequest用import type。 Next.js 16 / TypeScript 会标记只当类型用的值导入;import type让产物更干净。- 密钥放 header 而不是 query。 header 不会出现在 CDN 访问日志里。
- 路径不带末尾斜杠。
revalidatePath匹配的是路由文件结构,不是地址栏里的 URL,所以无论trailingSlash怎么配,/en/articles/my-slug都是对的。字面路径(不含[slug]段)时第二个type参数可以省。 - 双语路径。 传
lang让 EN 和 ZH 独立 revalidate,省掉全站刷新。
给缓存数据打 tag,revalidateTag 才有用
revalidateTag 只对打了对应 tag 的数据生效。Next.js 16 里有两种打 tag 的方式,都能用。
如果是从外部 CMS API 拉数据,就给 fetch 打 tag:
// lib/content.ts
export async function getArticle(slug: string) {
const res = await fetch(`${process.env.CMS_URL}/articles/${slug}`, {
next: { tags: [`article:${slug}`, 'articles'] },
});
return res.json();
}
如果用已转正的 'use cache' 指令缓存整个函数(数据库查询、读 Markdown),就用 cacheTag 打 tag:
// lib/content.ts
import { cacheTag } from 'next/cache';
export async function getAllArticles() {
'use cache';
cacheTag('articles');
const rows = await db.article.findMany({ orderBy: { publishedAt: 'desc' } });
return rows;
}
两种方式下,revalidateTag('article:my-slug', 'max') 只刷新那一篇的数据;revalidateTag('articles', 'max') 刷新列表页和所有读了 articles tag 的文章。
配 CMS webhook
Sanity / Contentful / Strapi / Storyblok 都是同一套:
URL: https://yourdomain.com/api/revalidate
Method: POST
Headers: x-revalidate-secret: <env REVALIDATE_SECRET>
Body: { "slug": "{{slug}}", "lang": "{{lang}}" }
大多数 CMS 在 webhook 模板里能取到 {{slug}} 和语言变量。取不到就传文档 ID,在 route handler 服务端查 slug。
端到端测一遍
宣告完成前三件事要测:
# 1. 带正确密钥能用
curl -sX POST https://yourdomain.com/api/revalidate \
-H "x-revalidate-secret: $REVALIDATE_SECRET" \
-H "content-type: application/json" \
-d '{"slug":"my-test-article","lang":"en"}'
# {"ok":true,"revalidated":{"path":"/en/articles/my-test-article"}}
# 2. 没密钥被拒
curl -sX POST https://yourdomain.com/api/revalidate \
-H "content-type: application/json" \
-d '{"slug":"my-test-article"}'
# {"ok":false,"error":"unauthorized"}
# 3. 页面确实更新了
curl -s https://yourdomain.com/en/articles/my-test-article/ | grep "key phrase from edit"
容易踩的坑
- 把密钥写在 URL 里(
/api/revalidate?secret=xxx)。会泄漏到 CDN 日志、Vercel analytics、外链的 HTTPReferer。用 header。 - 动态路由模式没带
type参数。revalidatePath('/articles/[slug]')会抛错,得写成revalidatePath('/articles/[slug]', 'page')。只要路径里有[段]就必须带type;字面路径如/articles/my-post则不用。 - 调
revalidatePath('/articles')指望它刷新/articles/foo。page级 revalidation 不会级联到子页。要么用'layout'(它会让嵌套子页一起失效),要么用共享revalidateTag。 - 在 webhook 路由里调
updateTag。它只能在 Server Action 里用,会抛updateTag can only be called from within a Server Action。route handler 里用revalidateTag;updateTag留给需要”立刻看到自己刚写的”的站内发布动作。 - 数据上打的是
'posts',revalidateTag('post')调成了单数。tag 区分大小写,typo 不报错,静默失效。 - 指望从 route handler 立即刷新。在 webhook 里
revalidatePath只是把路径标记为过期,重建发生在下次访问,而不是调用那一刻。访问一次页面(或预热)才能确认。 - 自动保存(每敲一下键)触发 webhook 进入死循环。加限流或 debounce。
- 没测试回滚:发错版本后能不能再用 CMS 的上一版状态触发一次 revalidate?值得演练。
FAQ
- 什么时候该用
updateTag而不是revalidateTag?: 只在 Server Action 里、且同一个用户需要立刻看到自己刚做的改动(read-your-own-writes)时用,比如提交表单后立即回显。它会让缓存过期、下次请求等新数据。webhook 或任何 route handler 里都用不了它,得用revalidateTag(tag, 'max')——它在新页面渲染时继续给旧页面。 - Cloudflare 或 Netlify 上能用 on-demand revalidation 吗?: 越来越能。Next.js 16.2 联合 OpenNext、Netlify、Cloudflare 推出了稳定的 Adapter API,平台不用再逆向构建产物。Netlify 适配器支持按需和定时 revalidation;Cloudflare 上用 OpenNext 适配器(旧的
next-on-pages已弃用)。依赖前先在自己的环境里测一遍。 revalidatePath和router.refresh()区别?:revalidatePath让所有用户的服务端缓存失效。router.refresh()只让当前客户端重新拉数据,服务端什么都不清。- 能在 Server Action 里 revalidate 吗?: 能,而且站内发布动作放 Server Action 更干净。webhook 路由留给外部触发(CMS、GitHub commit)。
- Pages Router 怎么做?: API 路由里
res.revalidate('/path')。概念一样,API 不同。本文这些next/cache函数只用于 App Router。 - revalidate 会刷新 metadata(title、OG 图)吗?: 会。metadata 是路由渲染的一部分,revalidate 后的第一次请求会连着页面一起拿到新的 metadata。
官方参考留着备查:revalidatePath 和 revalidateTag。