Next.js 缓存

Next.js 缓存

最后更新:2026 年 8 月 25 日。

本页介绍 Cache Components 模型下的缓存。需在 next.config.ts 文件中设置 cacheComponents: true 才能启用。如果没有使用 Cache Components,请参阅缓存与重新验证:旧模型指南。

缓存是一种保存数据获取及其他计算结果的技术。后续请求需要相同数据时,可以更快地返回结果,而无需重复完成这些工作。

启用 Cache Components

在 Next 配置文件中添加 cacheComponents 选项,即可启用 Cache Components:

文件:next.config.ts

import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  cacheComponents: true,
}

export default nextConfig

提示:启用 Cache Components 后,GET 路由处理程序采用与页面相同的预渲染模型。详见使用 Cache Components 的路由处理程序。

用法

use cache 指令会缓存异步函数和组件的返回值,可以在两个层级使用:

  • 数据层:缓存获取或计算数据的函数,例如 getProducts()、getUser(id)。
  • UI 层:缓存整个组件或页面,例如 async function BlogPosts()。

缓存指令会为结果指定生命周期,Next.js 利用这项信息实施渲染优化。参阅预渲染,了解缓存结果如何成为静态外壳的一部分,以及如何被纳入预取。

提示:Next.js 文档团队建议为每个缓存指令搭配一个 cacheLife。如果没有指定,就会应用隐式的 default 配置。

函数参数以及从父级作用域捕获的值,都会自动成为缓存键的一部分。因此,不同输入会产生独立的缓存条目。缓存输出介绍了条目包含的内容;有关哪些值可以缓存,以及参数如何工作,请参阅序列化要求与限制。

数据层缓存

要缓存用于获取数据的异步函数,在函数体顶部添加 use cache 指令:

文件:app/lib/data.ts

import { cacheLife } from 'next/cache'

export async function getUsers() {
  'use cache'
  cacheLife('hours')
  return db.query('SELECT * FROM users')
}

如果多个组件使用同一份数据,或者希望将数据与 UI 分开缓存,数据层缓存就很有用。

UI 层缓存

要缓存整个组件、页面或布局,在组件或页面函数体顶部添加 use cache 指令:

文件:app/page.tsx

import { cacheLife } from 'next/cache'

export default async function Page() {
  'use cache'
  cacheLife('hours')

  const users = await db.query('SELECT * FROM users')

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

如果在文件顶部添加 use cache,该文件导出的所有函数都会被缓存。

流式传输未缓存的数据

如果组件从 API、数据库或其他异步操作等异步来源获取数据,并且每次请求都需要最新数据,就不要使用 "use cache"。

应将组件包在 <Suspense> 中,并提供后备 UI。后备 UI 会随预渲染外壳一起发送,异步工作则在请求时运行。

文件:page.tsx

import { Suspense } from 'react'

async function LatestPosts() {
  const data = await fetch('https://api.example.com/posts')
  const posts = await data.json()
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

export default function Page() {
  return (
    <>
      <h1>My Blog</h1>
      <Suspense fallback={<p>Loading posts...</p>}>
        <LatestPosts />
      </Suspense>
    </>
  )
}

例如,<p>Loading posts...</p> 会包含在静态外壳中,文章内容则在请求时以流式方式传入。

如果未缓存的读取操作外没有 <Suspense> 边界,开发浮层会显示 blocking-route 诊断,并提供以下修复方案:

流式传输:包在 Suspense 中,或移入 Suspense。修复卡片示例:<Suspense fallback={…}> <DataChild /> </Suspense>。

提示:每张修复卡片都链接到详细说明,包含模式、代码示例及取舍。点击卡片即可深入了解。

<Suspense> 在异步工作完成前提供后备 UI,但它本身不会让组件改为动态渲染。如果组件只执行同步工作,无论是否包在 <Suspense> 中,都会在预渲染期间完成。

使用运行时 API

运行时 API 需要的信息,只有在用户发起请求后才能取得,包括:

访问运行时 API 的组件应包在 <Suspense> 中:

文件:page.tsx

import { cookies } from 'next/headers'
import { Suspense } from 'react'

async function UserGreeting() {
  const cookieStore = await cookies()
  const theme = cookieStore.get('theme')?.value || 'light'
  return <p>Your theme: {theme}</p>
}

export default function Page() {
  return (
    <>
      <h1>Dashboard</h1>
      <Suspense fallback={<p>Loading...</p>}>
        <UserGreeting />
      </Suspense>
    </>
  )
}

如果访问运行时 API 时没有 <Suspense>,开发浮层会显示相同的 blocking-route 诊断,并提供相同的修复方案:

流式传输:包在 Suspense 中,或移入 Suspense。修复卡片示例:<Suspense fallback={…}> <DataChild /> </Suspense>。

依赖运行时信息的数据仍可以通过 "use cache: private" 获得缓存生命周期。这是 Cache Components 提供的另一种变体,它能为直接读取 Cookie、请求头或 searchParams 的函数赋予生命周期,使结果可以被纳入预取。

下一节展示 use cache: private 之外的另一种方式:提取运行时值,再将其传给共享的缓存函数。

将运行时值传给缓存函数

可以从运行时 API 中提取值,并将它们作为参数传给缓存函数:

文件:app/profile/page.tsx

import { cookies } from 'next/headers'
import { Suspense } from 'react'

export default function Page() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <ProfileContent />
    </Suspense>
  )
}

// Component (not cached) reads runtime data
async function ProfileContent() {
  const session = (await cookies()).get('session')?.value
  return <CachedContent sessionId={session} />
}
// Cached component receives extracted value as a prop
async function CachedContent({ sessionId }: { sessionId: string }) {
  'use cache'
  // sessionId becomes part of the cache key
  const data = await fetchUserData(sessionId)
  return <div>{data}</div>
}

请求到来时,如果找不到匹配的缓存条目,<CachedContent /> 就会执行,并保存结果,供后续具有相同 sessionId 的请求使用。

提示:由于 <CachedContent /> 必须等待请求数据,它不会被加入预渲染的静态外壳。在运行时,它默认缓存在内存中,无法跨无服务器请求持久保留,所以可能在每次请求时重新计算。要获得持久、共享的缓存,应使用 'use cache: remote'。

在这种模式下,预取可以在客户端跳转过程中,使用用户的实际会话预渲染 <CachedContent />,在点击之前就准备好结果。即使服务器缓存条目很少能保留到下次请求,这种方式仍然有效:指定的生命周期使结果能够加入预取,而客户端会在 cacheLife 的 stale 时间窗口内将其视为新鲜数据。

静态内容、缓存与流式传输

下面的完整示例展示静态内容、缓存的动态内容和流式动态内容如何在同一页面中协作:

文件:app/blog/page.tsx

import { Suspense } from 'react'
import { cookies } from 'next/headers'
import { cacheLife, cacheTag } from 'next/cache'
import Link from 'next/link'

export default function BlogPage() {
  return (
    <>
      {/* Static content - prerendered automatically */}
      <header>
        <h1>Our Blog</h1>
        <nav>
          <Link href="/">Home</Link> | <Link href="/about">About</Link>
        </nav>
      </header>
      {/* Cached dynamic content - included in the static shell */}
      <BlogPosts />

      {/* Runtime dynamic content - streams at request time */}
      <Suspense fallback={<p>Loading your preferences...</p>}>
        <UserPreferences />
      </Suspense>
    </>
  )
}

type Post = { id: string; title: string; author: string; date: string }

// Everyone sees the same blog posts (revalidated every hour)
async function BlogPosts() {
  'use cache'
  cacheLife('hours')
  cacheTag('posts')

  const res = await fetch('https://api.vercel.app/blog')
  const posts: Post[] = await res.json()

  return (
    <section>
      <h2>Latest Posts</h2>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <h3>{post.title}</h3>
            <p>
              By {post.author} on {post.date}
            </p>
          </li>
        ))}
      </ul>
    </section>
  )
}

// UI that depends on a value stored in cookies
async function UserPreferences() {
  const theme = (await cookies()).get('theme')?.value || 'light'
  const favoriteCategory = (await cookies()).get('category')?.value

  return (
    <aside>
      <p>Your theme: {theme}</p>
      {favoriteCategory && <p>Favorite category: {favoriteCategory}</p>}
    </aside>
  )
}

预渲染期间,页头(静态内容)和博客文章(通过 use cache 缓存)都会成为静态外壳的一部分,用户偏好设置的后备 UI 也会包含其中。Cookie 中保存的界面偏好设置则在请求时流式传入。

与旧渲染模型不同,在这里读取 cookies() 不会让整个路由都改为动态渲染。Suspense 边界在运行时数据流入的位置提供后备 UI,而静态和缓存内容仍然包含在最初的 HTML 中。

正如 <Suspense> 用来包住异步访问,错误边界用来限制错误的影响范围:可以将错误边界包在渲染时可能出错的子树外。组件级边界使用 catchError,路由级边界使用 error.js 文件约定。

开发时还要考虑:在 generateMetadata 和 generateViewport 内,未缓存的数据获取或运行时数据访问,会触发与页面内相同的诊断和错误,帮助你实现预期的渲染方式。若要为已知和未知参数值实现增量静态再生成,请参阅配合 Cache Components 的 ISR。

随机值与时间戳

Math.random()、Date.now() 或 crypto.randomUUID() 等操作每次执行都会产生不同的值。Cache Components 要求你明确处理这类操作。

提示:performance.now() 用于遥测,因此 Next.js 不把它视为需要防护的值。可用它计时,并将结果传给日志或指标系统,而不是直接渲染出来。

要为每次请求生成唯一值,应在这些操作之前调用 connection(),将执行推迟到请求时,并把组件包在 <Suspense> 中:

文件:page.tsx

import { connection } from 'next/server'
import { Suspense } from 'react'

async function UniqueContent() {
  await connection()
  const uuid = crypto.randomUUID()
  return <p>Request ID: {uuid}</p>
}

export default function Page() {
  return (
    <Suspense fallback={<p>Loading...</p>}>
      <UniqueContent />
    </Suspense>
  )
}

也可以缓存结果,让所有用户在重新验证之前看到相同的值:

文件:page.tsx

export default async function Page() {
  'use cache'
  const buildId = crypto.randomUUID()
  return <p>Build ID: {buildId}</p>
}

无需记住哪些操作具有这种行为。开发浮层会根据调用类型显示 blocking-prerender-random、blocking-prerender-current-time 或 blocking-prerender-crypto 诊断,并给出以下修复方案:

动态生成:每次请求都生成。修复卡片示例:await connection() const id = Math.random() return <Item id={id} />。

缓存:缓存该值。修复卡片显示的片段:function RandomId() { "use cache" return String(Math.random())。

可预测的值

随机值和时间戳可能在不同渲染之间变化;模块导入、同步 I/O 和纯计算则每次运行都会产生相同结果。仅使用这些操作的组件会自动预渲染,其输出在构建时成为静态 HTML 的一部分。

文件:page.tsx

import fs from 'node:fs'

export default async function Page() {
  const constants = await import('./constants.json')
  const content = fs.readFileSync('./config.json', 'utf-8')
  const items = JSON.parse(content).items ?? []

  return (
    <div>
      <h1>{constants.appName}</h1>
      <ul>
        {items.map((item) => (
          <li key={item.id}>{item.value}</li>
        ))}
      </ul>
    </div>
  )
}

提示:这也包括具有同步 API 的嵌入式数据库查询,例如 better-sqlite3 或 Node.js 内置的 node:sqlite。如果需要从同步来源获取每次请求的数据,应在查询之前调用 connection()。

有些异步 API 读取不依赖传入请求的本地资源,例如字体或配置文件。如果这些资源预期对每次请求都相同,应在模块作用域读取一次,而不是在渲染期间读取。

如果数据应在渲染期间计算,并跨请求重复使用,则用 use cache 包住读取操作。如果数据依赖传入请求,或者预期会随时间变化,则在请求时渲染过程中读取。

文件:page.tsx

import { readFile } from 'node:fs/promises'

const content = await readFile('./config.json', 'utf-8')
const items = JSON.parse(content).items ?? []

export default function Page() {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>{item.value}</li>
      ))}
    </ul>
  )
}

这个示例中,配置文件预期对每次请求都相同,所以在模块作用域只读取一次。如果在组件内调用 await readFile(),它会被当作未缓存数据,必须在 use cache 内访问,或置于 <Suspense> 边界之后。由于这个文件不依赖请求,而且预期不会变化,模块作用域是最简单的选择。

预渲染

构建时,Next.js 会渲染路由的组件树。每个组件如何处理,取决于它使用的 API:

  • use cache:只要结果的生命周期不是过短,就会缓存结果,并将其纳入静态外壳。
  • <Suspense>:后备 UI 被纳入静态外壳,内容则在请求时流式传入。
  • 可预测的值:模块导入、fs.readFileSync 和纯计算会在预渲染期间完成,并自动纳入静态外壳。
  • 随机值与时间戳:使用 connection() 配合 <Suspense>,为每次请求生成唯一值;或者使用 use cache,让用户共享同一值。

这样会生成静态外壳,其中包含用于首次加载的 HTML,以及用于客户端导航的序列化 RSC Payload。无论用户直接访问 URL,还是从其他页面跳转,浏览器都能立即收到已经渲染好的内容。这种方式称为部分预渲染(Partial Prerendering,PPR),也是 Cache Components 的默认行为。

部分预渲染的商品页:导航及商品信息为静态内容,购物车和推荐商品为动态内容
部分预渲染的商品页:导航及商品信息为静态内容,购物车和推荐商品为动态内容

生成的每个静态外壳都可以直接通过 CDN 提供,无需访问上游服务器,从而让直接导航即时完成。

路由静态外壳具体包含什么,取决于构建时已知的信息。如果路由的动态参数已知,外壳就包含具体内容,而剩余的未缓存或运行时数据仍在 <Suspense> 后备 UI 之后流式传入。如果参数未知,可复用且与 URL 无关的版本就是 App Shell:同一个静态外壳中,依赖参数的部分仍由后备 UI 占位。

增量静态再生成会在首次访问后补全具体版本。

Next.js 要求明确处理无法在预渲染期间完成的组件。它会在开发浮层和开发服务器控制台显示验证诊断,指出路由并提供修复方向:缓存访问、将访问移到 <Suspense> 边界中,或者让该路由退出这一模式。这项验证使每个路由都能生成静态外壳,从而保持即时的直接导航体验。

客户端部分渲染页面示意图:正在流式传输的内容块由加载 UI 占位
客户端部分渲染页面示意图:正在流式传输的内容块由加载 UI 占位

🎥 观看:为何使用部分预渲染,以及它如何工作——YouTube,10 分钟。

尽可能扩大静态外壳

异步工作在组件树中的位置越深,页面能够预渲染的部分就越多。这是 Cache Components 鼓励的结构模式,值得普遍采用,也是后文即时导航和预取的基础。它适用于所有运行时 API,以及数据获取等异步操作。

考虑下面这个在顶层解构 params 的布局:

文件:app/shop/[slug]/layout.tsx

export default async function Layout({
  children,
  params,
}: LayoutProps<'/shop/[slug]'>) {
  const { slug } = await params

  return (
    <div>
      <Sidebar />
      <h1>{slug}</h1>
      {children}
    </div>
  )
}

如果这个参数是动态的,也就是没有由 generateStaticParams 提供,那么它属于运行时数据,该布局便无法预渲染。

不过,通常可以在组件树更深的位置读取参数值。无需在布局层等待,改为把 params Promise 向下传递,并在那里等待:

文件:app/shop/[slug]/layout.tsx

import { Suspense } from 'react'

// Not async: this layout never awaits params
export default function Layout({
  children,
  params,
}: LayoutProps<'/shop/[slug]'>) {
  return (
    <div>
      <Sidebar />
      <Suspense fallback={<h1>Loading...</h1>}>
        {/* await happens inside the boundary, so the shell still renders */}
        {params.then(({ slug }) => (
          <SlugHeading slug={slug} />
        ))}
      </Suspense>
      {children}
    </div>
  )
}

function SlugHeading({ slug }: { slug: string }) {
  return <h1>{slug}</h1>
}

现在,<Sidebar />、{children} 和 Suspense 后备 UI 都成为静态外壳的一部分,只有 SlugHeading 会在请求时流式传入。也可以把整个 params Promise 传给子组件,再在子组件中等待它。

相同原则适用于 cookies()、headers()、searchParams 和数据获取。相关模式见使用 React.cache 复用数据。

即时导航

Cache Components 在 16.0.0 中推出时,已经验证直接访问路由是否会生成静态外壳。客户端导航则有所不同:覆盖直接访问场景的 <Suspense> 边界,未必会参与跳转时的渲染。有框架帮助,便更容易安排正确的结构。现在 Cache Components 也会验证这些导航,通过诊断和错误提示,指导你让前往路由的导航即时完成。

例如,将数据包在 <Suspense> 中,通过 use cache 缓存数据,或者调整访问发生的位置。

示例与检查工具见即时导航指南。

预取

启用部分预取(Partial Prefetching)后,路由器默认预取每个路由的 App Shell。App Shell 包含静态内容,以及从 cookies() 和 headers() 派生的会话数据。如果还要预取依赖链接 URL 数据的缓存内容,例如 searchParams 或动态 params,应在该链接上设置 prefetch={true}。

当 <Link prefetch={true}> 指向启用了部分预取的路由时,Next.js 会在预取时再次渲染该路由的组件树,此时目标 URL 已解析完成。规则仍然相同,但由于 searchParams 和 params 已经可用,组件树中更多部分能够得到解析:

  • 使用从运行时 API 提取的值作为参数调用的 use cache,会加入针对该链接的预取。
  • use cache: private 在服务器上执行,直接读取运行时数据,并将结果作为针对该链接预取的一部分,缓存在浏览器中。
  • <Suspense> 的后备 UI 保留在预取的界面中,未缓存内容则在请求时流式传入。

这种逐链接预取会包含在目标 URL 已知后才解析完成的缓存内容。每个可预取链接都需要一次服务器调用。

例如,下面的搜索页面会从 URL 读取 searchParams:

文件:app/search/page.tsx

import { Suspense } from 'react'

export default function SearchPage(props: PageProps<'/search'>) {
  return (
    <Suspense fallback={<p>Loading results...</p>}>
      <Results searchParams={props.searchParams} />
    </Suspense>
  )
}

async function Results({
  searchParams,
}: Pick<PageProps<'/search'>, 'searchParams'>) {
  const { q } = await searchParams
  const results = await search(q)
  return (
    <ul>
      {results.map((result) => (
        <li key={result.id}>{result.title}</li>
      ))}
    </ul>
  )
}

async function search(query: string | string[] | undefined) {
  'use cache'
  return db.search(query)
}

直接访问时,<Results> 会在后备 UI 之后流式传入。

当指向 /search?q=shoes 的 <Link> 被预取时,框架会从链接 URL 解析 searchParams,因此缓存的 search 结果会在点击之前就包含在运行时预渲染中。浏览器随后复用这个结果,直到其 stale 时间到期,或 searchParams 发生变化。

参阅采用部分预取,了解 <Link> 预取的行为及采用方式。

完整模式见优化预取指南,所有模式见 prefetch 参考。

缓存内容保存在哪里

缓存函数的输出会在构建时或运行时序列化为 RSC 载荷。其他操作都以这个载荷为基础:Next.js 将它渲染为 HTML,保存在服务器或远程存储中,或者发送给浏览器。cacheLife 决定每份副本保持新鲜的时间:

  • 预渲染 HTML:载荷被渲染为 HTML;自行托管时保存在磁盘上,或者保存在平台 CDN 后面的持久存储中。构建时,这份 HTML 是静态外壳;经过 ISR 升级后,它是具体页面。重建时机由 revalidate 和 expire 控制。
  • 共享存储:默认情况下,结果保存在每个实例各自的内存存储中,在无服务器环境中是临时的。use cache: remote 可将结果移到跨实例共享的持久缓存处理程序中;这会增加一次网络往返,只有命中率较高时才划算。
  • 浏览器:载荷被纳入客户端导航或预取所发送的 RSC,浏览器在其 stale 时间窗口内将它视为新鲜数据。use cache: private 的结果只存在于这里。

提示:读取 cookies() 或 headers() 的 App Shell 与会话有关,按会话缓存在客户端,而不是放进共享服务器缓存。

这些存储的作用域都限制在单次部署内。新部署会重新开始,生成新的预渲染结果,use cache 条目不会沿用;即使是持久的 remote 条目也不例外,因为缓存键包含构建 ID。不同环境的行为见运行时缓存注意事项;服务器缓存配置见自行托管。

增量静态再生成

对于包含动态参数段的路由,generateStaticParams 会在构建时预渲染列出的 URL。其他 URL 会立即收到 App Shell,然后利用此时已经明确的参数在后台升级,并缓存起来供下一位访问者使用。

完整教程见配合 Cache Components 的 ISR。

机器人与爬虫

浏览器会立即收到静态外壳。机器人和爬虫则根据用户代理识别,并采用不同处理方式:由于它们需要完整文档,Next.js 会跳过外壳,在请求时动态渲染整个页面,等渲染结束后再发送完整 HTML。

由于外壳会重新渲染,而不是复用,原本在预渲染期间完成的工作,现在会针对机器人在请求时运行。如果外壳中的部分内容依赖仅在预渲染期间存在的输入,例如构建时数据,或请求时环境无法访问的值,那么一个人类用户能正常加载的页面,可能无法为爬虫渲染。应确保外壳依赖的数据在请求时也可用。详见流式传输指南中的机器人与爬虫。

后续阅读

进一步了解重新验证,以及本页提到的 API。

  • 重新验证:了解如何使用基于时间及按需触发的策略重新验证缓存数据。
  • use cache:了解如何使用该指令在 Next.js 应用中缓存数据。
  • cacheComponents:了解如何启用 Next.js 的 cacheComponents 标志。
  • 即时导航:了解如何组织应用,以预取和预渲染更多内容,实现即时页面加载与客户端导航。

来源:Next.js 官方文档:Caching。作者:Next.js 文档团队及贡献者;页面显示最后更新于 2026 年 8 月 25 日,读取时文档版本选择器显示 16.3.8。

本文为该页面的完整中文翻译,调整语言与排版,保留示例代码、注释、修复卡片片段、图片和延伸阅读。代码及卡片片段依源页呈现原样保留。图片来源于原文链接。

文档所在的 vercel/next.js 仓库采用 MIT 许可,许可正文明确包含相关文档文件;许可及版权声明如下:

The MIT License (MIT)

Copyright (c) 2025 Vercel, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容