Next.js App Router 数据获取:fetch 缓存、revalidate 与 dynamic 一次讲清 Next.js App Router 数据获取:fetch 缓存、revalidate 与 dynamic 一次讲清从 Pages Router 迁到 App Router 的人,几乎都被同一件事绊倒过:「我明明改了数据库,页面刷新了半天还是旧数据」,或者反过来「本地跑得好好的,一部署到线上数据就不更新了」。根源都在同一处 —— App Router 把fetch悄悄改造了,默认帮你做缓存,而你没意识到。这篇把 App Router 里数据到底什么时候缓存、什么时候刷新、怎么手动控制讲透,给出能直接照抄的配置。第一个反直觉:fetch 默认被缓存了在 App Router 的服务端组件里,Next.js 用自己的实现包了原生fetch。看这段代码:// app/products/page.tsx —— 默认就是服务端组件 export default async function ProductsPage() { // 注意:这个请求结果默认会被永久缓存 const res await fetch(https://api.example.com/products); const products await res.json(); return ( ul {products.map((p: any) ( li key{p.id}{p.name}/li ))} /ul ); }在生产构建下,这个fetch的结果默认等价于cache: force-cache—— 结果会被缓存,后续请求直接命中缓存,哪怕后端数据已经变了页面也不更新。这就是「改了数据库页面还是旧的」的元凶。提示:Next.js 14 里未配置的fetch默认强缓存;从 Next.js 15 起默认改成了不缓存(no-store)。但依赖「默认值」是最坑的写法 —— 版本一升行为就反转。所以别赌默认值,永远显式声明缓存策略。三种缓存策略,对号入座fetch的第二个参数直接决定缓存行为,一共就三种,记住场景就不会错:// 1) 永久缓存:适合几乎不变的数据,比如文档、配置、静态内容 await fetch(url, { cache: force-cache }); // 2) 完全不缓存:每次请求都打后端,适合购物车、实时价格、用户私有数据 await fetch(url, { cache: no-store }); // 3) 定时重新验证:缓存 60 秒,过期后台自动刷新,适合博客/商品列表这种可以旧一会儿的 await fetch(url, { next: { revalidate: 60 } });选择标准很简单:数据几乎不变 →force-cache;数据必须实时、每次都要最新 →no-store;数据允许有几十秒延迟,但要兼顾性能 →next: { revalidate: N }。第三种是最常用的,它用的是stale-while-revalidate策略:60 秒内直接给缓存(快),过期后第一个访问的人拿到的还是旧数据,但后台会悄悄拉新数据换掉缓存,下一个人就看到新的了。既快又不会太旧。revalidate 也可以写在页面级别如果一个页面里有多个 fetch,或者数据来自数据库(不走 fetch),用页面级的export const revalidate更省事。它作用于整个路由段:// app/blog/page.tsx // 整个页面每 300 秒重新生成一次 export const revalidate 300; export default async function BlogPage() { const posts await db.post.findMany(); // 直接查库,不是 fetch return PostList posts{posts} /; }revalidate 300表示这个页面走 ISR(增量静态再生):构建时生成一次 HTML,之后每 300 秒最多重新生成一次。适合内容更新不频繁的博客、落地页。dynamic:强制整页动态渲染有时你想让整个页面每次请求都实时渲染(比如后台管理页、依赖登录态的页面),不管里面 fetch 怎么配。用export const dynamic:// app/dashboard/page.tsx // force-dynamic:每次请求都在服务端实时渲染,禁用一切缓存 export const dynamic force-dynamic; export default async function Dashboard() { const stats await fetch(https://api.example.com/stats, { cache: no-store, }).then((r) r.json()); return StatsPanel data{stats} /; }dynamic有几个常用值:auto(默认):Next.js 自己判断能不能静态化;force-dynamic:强制每次请求动态渲染,等于给整页所有 fetch 兜底加no-store;force-static:强制静态,即使用了no-store也会被当成缓存。另外一个隐藏规则要记住:只要你在组件里用了cookies()、headers()、searchParams这些运行时才知道的东西,整个路由会自动变成动态渲染,不需要手动写force-dynamic。这也是很多人「以为是静态页,结果每次都在服务端跑」的原因。按需刷新:数据变了立刻更新,不用等 revalidate定时刷新有个缺点:用户在后台改了商品,前台要等 60 秒才生效。更好的做法是 —— 改完数据后主动通知 Next.js「这个缓存失效了」。用revalidatePath或revalidateTag:// app/actions.ts use server; import { revalidatePath, revalidateTag } from next/cache; export async function updateProduct(id: string, data: FormData) { await db.product.update({ where: { id }, data: { /* ... */ } }); // 方式一:让某个路径的缓存立即失效,下次访问重新生成 revalidatePath(/products); // 方式二:让打了特定 tag 的所有 fetch 失效(更精准) revalidateTag(products); }配合revalidateTag使用时,在 fetch 上打标签:// 给这个请求打上 products 标签 await fetch(https://api.example.com/products, { next: { tags: [products] }, });这样你可以设一个很长的revalidate(甚至永久缓存),平时享受满速缓存,只在真正改数据时用revalidateTag(products)精准击穿相关缓存。这是生产环境最推荐的组合:默认缓存 写操作后按需失效。小结App Router 里的fetch默认带缓存(且默认值随版本变),别赌默认值,永远显式写缓存策略。三选一:不变用force-cache,实时用no-store,能容忍延迟用next: { revalidate: N }(stale-while-revalidate,又快又够新)。页面级用export const revalidate控制 ISR;用cookies()/headers()会自动转动态渲染;export const dynamic force-dynamic强制整页实时。最佳实践:长缓存 revalidateTag/revalidatePath按需失效,改完数据立刻生效,平时全走缓存。一句话记忆:缓存策略要么写在 fetch 上,要么写在页面 export 上,改完数据用 revalidate 系列主动通知失效。