Back

Next.js 中的智能缓存:部分渲染与可复用组件

Next.js 中的智能缓存:部分渲染与可复用组件

你已经构建了一个 Next.js App Router 应用程序。数据获取正常工作,页面正常渲染。但你不确定缓存策略是否正确——或者是否有缓存策略。你可能遇到过陈旧数据意外出现的情况,看到同一个数据库查询在单次请求中多次触发,并且疑惑为什么某些路由尽管是”静态”的却感觉很慢。

本文将解释 Next.js App Router 数据缓存的实际工作原理、三个缓存层如何交互,以及如何构建封装了数据获取和缓存策略的可复用服务器组件。我们还将介绍 Next.js 部分预渲染(Partial Prerendering)以及 React 19 部分渲染如何实现组件级缓存策略。

核心要点

  • Next.js 使用三个缓存层(数据缓存、完整路由缓存、路由器缓存),它们在请求期间级联协作
  • 组件应该使用 React 的 cache() 进行去重,使用 fetch 选项控制生命周期,从而拥有自己的缓存策略
  • 基于标签的重新验证可以实现跨多个路由的精确缓存失效
  • 部分预渲染(PPR)允许通过 Suspense 边界将静态外壳与动态流式内容混合使用

三个缓存层如何协同工作

Next.js 缓存通过三种不同的机制运作,它们在每次请求期间相互作用。

数据缓存(Data Cache)

数据缓存在请求和部署之间持久化 fetch 结果。当你调用启用了缓存的 fetch 时,Next.js 会在服务器端存储响应。后续请求将返回缓存的数据,直到重新验证发生。

// 在所有请求中缓存 1 小时
const posts = await fetch('https://api.example.com/posts', {
  next: { revalidate: 3600 }
})

完整路由缓存(Full Route Cache)

在构建时,Next.js 将静态路由渲染为 HTML 和 RSC 载荷。这个完整路由缓存可以即时提供预渲染的内容。纯动态路由会跳过此缓存,但具有静态段或重新验证的路由仍然可以生成静态外壳。

客户端路由器缓存(Router Cache)

浏览器在导航期间将 RSC 载荷存储在内存中。布局在路由更改时保持不变。已访问的路由在会话期间缓存在内存中,并在导航时重用,直到失效。

这些层级联工作:使数据缓存失效会影响后续请求,完整路由缓存或路由器缓存会在下一次需要新数据的渲染时更新。

构建可复用的缓存服务器组件

防止陈旧数据错误的思维模型:组件应该拥有自己的缓存策略

// lib/data.ts
import { cache } from 'react'

export const getProduct = cache(async (id: string) => {
  const res = await fetch(`https://api.example.com/products/${id}`, {
    next: { revalidate: 300, tags: ['products', `product-${id}`] }
  })
  return res.json()
})

这个函数可以从任何组件调用——布局、页面或嵌套子组件。React 的 cache() 在单次渲染过程中对调用进行去重。next.revalidate 选项控制数据缓存的生命周期。标签(tags)可以实现精确失效。

在任何地方使用此函数,无需属性传递(prop drilling):

// app/products/[id]/page.tsx
export default async function ProductPage({ params }: { params: { id: string } }) {
  const { id } = await params
  const product = await getProduct(id)
  return <ProductDetails product={product} />
}

使用路由段选项控制缓存行为

除了 fetch 选项之外,路由段配置可以在路由级别控制缓存:

// 强制动态渲染
export const dynamic = 'force-dynamic'

// 为所有 fetch 设置重新验证周期
export const revalidate = 3600

对于按需失效,使用 revalidatePath 或基于标签的重新验证:

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

export async function updateProduct(id: string) {
  // await db.product.update(...)
  revalidateTag(`product-${id}`)
}

Next.js 部分预渲染:实验性方向

Next.js 部分预渲染(Partial Prerendering)代表了一个重大转变。PPR 不是在完全静态或完全动态路由之间选择,而是预渲染一个静态外壳,同时通过 Suspense 边界流式传输动态内容。

import { Suspense } from 'react'

export default async function ProductPage({ params }: { params: { id: string } }) {
  const { id } = await params
  
  return (
    <>
      {/* 静态外壳 - 预渲染 */}
      <Header />
      <ProductInfo id={id} />
      
      {/* 动态部分 - 在请求时流式传输 */}
      <Suspense fallback={<CartSkeleton />}>
        <UserCart />
      </Suspense>
    </>
  )
}

静态部分即时提供。动态部分在解析时流式传输。这个功能仍处于实验阶段——通过在 next.config.js 的 experimental 选项中设置 ppr: true 来启用——但它指向了 Next.js 缓存的未来方向。

React 19 与组件级缓存

React 19 改进的 Suspense 行为支持更细粒度的缓存策略。包裹在 Suspense 中的组件可以独立管理其数据生命周期。使用实验性的 use cachecacheLife API,你可以缓存组件子树而不是整个页面:

import { cacheLife } from 'next/cache'

async function BlogPosts() {
  'use cache'
  cacheLife('hours')
  
  const res = await fetch('https://api.example.com/posts')
  const posts = await res.json()
  return <PostList posts={posts} />
}

基于标签的重新验证让团队可以安全地跨路由共享缓存数据。在 /products/[id]/checkout 中使用的产品组件可以一次失效,在所有地方更新。

结论

构建封装自己缓存策略的服务器组件。使用 React 的 cache() 进行请求去重,使用 fetch 选项控制数据缓存,使用标签进行跨路由失效。部分预渲染仍处于实验阶段,但值得理解——这是 Next.js 缓存的发展方向。

目标不是最大化缓存,而是建立与数据实际新鲜度要求相匹配的可预测缓存。

常见问题

React 的 cache() 在单次渲染过程中对函数调用进行去重,防止在一次请求期间多次获取相同数据。Next.js fetch 缓存使用数据缓存在多个请求和部署之间持久化结果。两者结合使用:cache() 用于渲染时去重,fetch 选项用于跨请求持久化。

当需要在多个路由中使特定数据失效时使用 revalidateTag,例如在详情页和结账页都显示的产品信息。当想要使特定 URL 路径的所有缓存数据失效时使用 revalidatePath。标签提供更精确的控制,而路径对于单路由失效更简单。

检查构建输出中的路由指示符。静态路由显示圆圈图标,而动态路由显示 lambda 符号。你也可以在组件中添加 console.log 语句——如果每次请求都记录日志,则路由是动态的。使用 cookies()、headers() 或 searchParams 会自动将路由选择为动态渲染。

截至 Next.js 15,部分预渲染仍处于实验阶段。虽然可以启用它进行测试,但尚不建议在生产应用程序中使用。API 和行为可能在未来版本中发生变化。在生产环境采用之前,请关注 Next.js 文档和发布说明以获取稳定性更新。

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay