Next.js App Router 的缓存层次:从 fetch 缓存到完整路由缓存的四层模型
Next.js App Router 的缓存层次从 fetch 缓存到完整路由缓存的四层模型Next.js App Router 的缓存机制是影响应用性能和数据新鲜度的核心设计。理解其四层缓存模型——请求记忆化、数据缓存、完整路由缓存、路由器缓存——有助于避免数据过期、缓存穿透和构建体积膨胀等问题。一、四层缓存的架构总览Next.js 的缓存从底层的数据获取到顶层的客户端路由缓存形成了一个逐层递进的体系。每一层的失效和更新策略相互独立但上层缓存的有效性依赖下层缓存的新鲜度graph TB subgraph 客户端 A[路由器缓存br/Router Cache] end subgraph 服务端 B[完整路由缓存br/Full Route Cache] C[数据缓存br/Data Cache] D[请求记忆化br/Request Memoization] end E[外部 API / 数据库] D --|缓存 fetch 结果| E C --|缓存序列化数据| D B --|缓存 RSC Payloadbr/和 HTML| C A --|缓存 RSC Payloadbr/在客户端内存| B B -.-|revalidatePathbr/revalidateTag| C C -.-|revalidate 选项| D四层缓存按作用范围分为请求级、页面级和应用级。请求记忆化在同一渲染 Pass 内生效数据缓存在多个请求间共享完整路由缓存作用于构建或请求时生成的静态页面路由器缓存驻留在客户端浏览器内存。二、请求记忆化同一渲染 Pass 内的去重请求记忆化是 React 的cache()函数和fetch的自动去重机制共同实现的。在同一个页面渲染过程中多次相同的fetch请求会被自动合并import { cache } from react; /** * 数据库查询函数使用 React cache() 实现请求级去重 * 同一渲染 Pass 内多次调用只执行一次 */ const getProductById cache(async (id: string) { console.log([DB Query] 查询产品: ${id}); // 模拟数据库查询延迟 await new Promise((resolve) setTimeout(resolve, 100)); return { id, name: 产品 ${id}, price: Math.floor(Math.random() * 1000), }; }); /** * 产品详情页面多个组件独立调用 getProductById * 但在同一请求中只查询一次数据库 */ async function ProductPage({ params, }: { params: { id: string }; }) { // 即使 ProductTitle 和 ProductPrice 各自调用, // 同一渲染 Pass 内只执行一次数据库查询 return ( main ProductTitle productId{params.id} / ProductPrice productId{params.id} / ProductReviews productId{params.id} / /main ); } async function ProductTitle({ productId }: { productId: string }) { const product await getProductById(productId); return h1{product.name}/h1; } async function ProductPrice({ productId }: { productId: string }) { const product await getProductById(productId); return span¥{product.price}/span; }关键点在于请求记忆化只在同一渲染 Pass内生效。不同 HTTP 请求之间的渲染是独立的不会共享cache()的结果。如果需要在多个请求间共享数据需要使用下一层——数据缓存。三、数据缓存跨请求的数据持久化数据缓存是 Next.js 持久化fetch结果的内置机制。它的行为由cache和next.revalidate选项控制/** * 数据缓存的四种典型使用模式 */ // 模式 1: 强制缓存默认行为 // 构建时 fetch,结果永久缓存,除非手动失效 async function getStaticData() { const res await fetch(https://api.example.com/static-data, { cache: force-cache, // 默认值,可省略 }); return res.json(); } // 模式 2: 不缓存 // 每次请求都重新 fetch async function getDynamicData() { const res await fetch(https://api.example.com/dynamic-data, { cache: no-store, }); return res.json(); } // 模式 3: 定时重新验证ISR 模式 // 缓存结果,但每隔指定秒数后重新获取 async function getRevalidatedData() { const res await fetch(https://api.example.com/updates, { next: { revalidate: 60 }, // 每 60 秒重新验证 }); return res.json(); } // 模式 4: 按标签重新验证 // 通过 revalidateTag 精确控制缓存失效 async function getTaggedData(category: string) { const res await fetch( https://api.example.com/posts?category${category}, { next: { tags: [posts-${category}, all-posts], }, } ); return res.json(); } // 在 Server Action 或 Route Handler 中手动触发重新验证 import { revalidateTag, revalidatePath } from next/cache; /** * 发布新文章后的缓存刷新逻辑 */ export async function publishPost(formData: FormData) { use server; const category formData.get(category) as string; // 保存文章到数据库 await savePost(formData); // 精确失效只刷新相关分类的文章列表缓存 revalidateTag(posts-${category}); revalidateTag(all-posts); // 同时刷新页面路径的完整路由缓存 revalidatePath(/categories/${category}); revalidatePath(/); }数据缓存的实际挑战在于缓存粒度的选择。revalidateTag提供了比revalidatePath更精确的控制但需要在代码中建立清晰的标签命名约定。失控的标签会导致应该刷新的没刷新不应该刷新的反复刷。四、完整路由缓存与路由器缓存完整路由缓存在服务端存储渲染后的 RSC Payload 和 HTML。静态路由在构建时生成并永久缓存动态路由根据数据缓存的 revalidate 周期决定何时重新渲染。import { unstable_cache } from next/cache; /** * 使用 unstable_cache 手动控制路由缓存的片段 * 将数据获取和渲染结果分离缓存 */ const getCachedPageData unstable_cache( async (userId: string) { // 聚合多个数据源 const [profile, posts, followers] await Promise.all([ fetch(https://api.example.com/users/${userId}).then( (r) r.json() ), fetch( https://api.example.com/users/${userId}/posts ).then((r) r.json()), fetch( https://api.example.com/users/${userId}/followers ).then((r) r.json()), ]); return { profile, posts, followers }; }, [user-profile-page], // 缓存标签 { revalidate: 300, // 5 分钟后重新验证 tags: [user-profile], } ); /** * 用户主页组合缓存数据和实时数据 */ async function UserProfilePage({ params, }: { params: { userId: string }; }) { // 使用缓存数据渲染主要区域 const cachedData await getCachedPageData(params.userId); // 某些区域使用 no-store 获取实时数据 const onlineStatus await fetch( https://api.example.com/users/${params.userId}/online, { cache: no-store } ).then((r) r.json()); return ( div ProfileHeader data{cachedData.profile} / OnlineBadge status{onlineStatus} / PostList posts{cachedData.posts} / FollowerList followers{cachedData.followers} / /div ); }路由器缓存位于客户端在浏览器的内存中缓存已访问页面的 RSC Payload。它的独特之处在于有两个有效期静态页面缓存 5 分钟动态页面缓存 30 秒。在页面间导航时路由器缓存提供了即时的渲染结果避免了服务端重新获取。// 这四个操作会触发客户端路由器缓存失效 // 在 Server Action 中调用 use server; import { revalidatePath } from next/cache; import { redirect } from next/navigation; export async function updateProfile() { // 1. revalidatePath 同时失效服务端和客户端缓存 revalidatePath(/profile); // 2. redirect 会清除当前路由的客户端缓存 redirect(/profile); // 3. router.refresh() 在客户端调用,重新获取当前路由 // 4. Cookie 的 set/delete 操作会触发完整页面刷新 }路由器缓存的一个常见问题是用户在页面 A 执行了数据变更操作导航到页面 B 再返回页面 A 时看到的是缓存中的旧数据。解决方案是在变更操作后调用router.refresh()或在 Server Action 中使用revalidatePath。五、总结Next.js App Router 的四层缓存模型构成了一个完整的缓存谱系。请求记忆化解决同一渲染 Pass 内的重复请求问题对开发者完全透明。数据缓存通过cache选项和next.revalidate控制跨请求的数据持久化。完整路由缓存将渲染结果序列化存储决定了页面的静态/动态特性。路由器缓存在客户端提供即时导航体验但细粒度的失效策略是正确使用它的前提。在使用这些缓存时需要关注一个基本原则缓存的粒度应该与数据的变更频率匹配。高频率变更的数据使用no-store或短revalidate变更频率低的数据使用长revalidate或force-cache通过revalidateTag在数据变更时精确失效而不是依赖定时重新验证。错误的缓存策略会导致数据不一致而正确的分层缓存能够将页面加载时间从秒级降低到毫秒级。

相关新闻