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。看这段代码:

tsx 复制代码
// 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 的第二个参数直接决定缓存行为,一共就三种,记住场景就不会错:

tsx 复制代码
// 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 更省事。它作用于整个路由段:

tsx 复制代码
// 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:

tsx 复制代码
// 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「这个缓存失效了」。用 revalidatePathrevalidateTag:

tsx 复制代码
// 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 上打标签:

tsx 复制代码
// 给这个请求打上 '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 系列主动通知失效。

相关推荐
mldong20 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排21 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby606121 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角1 天前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!1 天前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰1 天前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi1 天前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen1 天前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马1 天前
AI源码分析:拆解模型底层实现逻辑
前端
IT_陈寒1 天前
React子组件莫名其妙重渲染?你可能漏了这个Hook
前端·人工智能·后端