跳到主要内容

如何实现增量静态再生成(ISR)

示例

增量静态再生成(ISR)使您能够:

  • 在不重建整个站点的情况下更新静态内容
  • 通过为大多数请求提供预渲染的静态页面来减少服务器负载
  • 确保自动为页面添加适当的 cache-control 头部
  • 处理大量内容页面而无需长时间的 next build 时间

以下是一个最小示例:

app/blog/[id]/page.tsx
interface Post {
id: string
title: string
content: string
}

// Next.js 将在请求到达时使缓存失效,
// 最多每 60 秒一次。
export const revalidate = 60

// 我们将在构建时仅预渲染来自 `generateStaticParams` 的参数。
// 如果请求的路径尚未生成,
// Next.js 将按需服务端渲染该页面。
export const dynamicParams = true // 或 false,对未知路径返回 404

export async function generateStaticParams() {
const posts: Post[] = await fetch('https://api.vercel.app/blog').then((res) =>
res.json()
)
return posts.map((post) => ({
id: String(post.id),
}))
}

export default async function Page({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const post: Post = await fetch(`https://api.vercel.app/blog/${id}`).then(
(res) => res.json()
)
return (
<main>
<h1>{post.title}</h1>
<p>{post.content}</p>
</main>
)
}
pages/blog/[id].tsx
import type { GetStaticPaths, GetStaticProps } from 'next'

interface Post {
id: string
title: string
content: string
}

interface Props {
post: Post
}

export const getStaticPaths: GetStaticPaths = async () => {
const posts = await fetch('https://api.vercel.app/blog').then((res) =>
res.json()
)
const paths = posts.map((post: Post) => ({
params: { id: String(post.id) },
}))

// 我们将在构建时仅预渲染这些路径。
// { fallback: 'blocking' } 将在路径不存在时
// 按需服务端渲染页面。
return { paths, fallback: false }
}

export const getStaticProps: GetStaticProps<Props> = async ({
params,
}: {
params: { id: string }
}) => {
const post = await fetch(`https://api.vercel.app/blog/${params.id}`).then(
(res) => res.json()
)

return {
props: { post },
// Next.js 将在请求到达时使缓存失效,
// 最多每 60 秒一次。
revalidate: 60,
}
}

export default function Page({ post }: Props) {
return (
<main>
<h1>{post.title}</h1>
<p>{post.content}</p>
</main>
)
}

这个示例的工作原理如下:

  1. next build 期间,所有已知的博客文章都会被生成(此示例中有 25 篇)
  2. 对这些页面的所有请求(例如 /blog/1)都会被缓存并立即响应
  3. 60 秒后,下一个请求仍会显示缓存的(过期的)页面
  4. 缓存失效,新版本的页面开始在后台生成
  5. 生成成功后,Next.js 将显示并缓存更新后的页面
  6. 如果请求 /blog/26,Next.js 将按需生成并缓存此页面

参考

路由段配置

函数

函数

示例

基于时间的重新验证

这会在 /blog 上获取并显示博客文章列表。一小时后,此页面的缓存在下次访问时失效。然后,在后台生成包含最新博客文章的新版本页面。

app/blog/page.tsx
interface Post {
id: string
title: string
content: string
}

export const revalidate = 3600 // 每小时失效一次

export default async function Page() {
const data = await fetch('https://api.vercel.app/blog')
const posts: Post[] = await data.json()
return (
<main>
<h1>博客文章</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</main>
)
}

我们建议设置较高的重新验证时间。例如,使用 1 小时而不是 1 秒。如果您需要更精确的控制,请考虑使用按需重新验证。如果您需要实时数据,请考虑切换到动态渲染

使用 revalidatePath 进行按需重新验证

对于更精确的重新验证方法,使用 revalidatePath 函数按需使页面失效。

例如,这个服务器操作会在添加新文章后被调用。无论您在服务器组件中如何检索数据,无论是使用 fetch 还是连接到数据库,这都会清除整个路由的缓存,并允许服务器组件获取新数据。

app/actions.ts
'use server'

import { revalidatePath } from 'next/cache'

export async function createPost() {
// 使缓存中的 /posts 路由失效
revalidatePath('/posts')
}

查看演示探索源代码

使用 revalidateTag 进行按需重新验证

对于大多数用例,建议重新验证整个路径。如果您需要更精细的控制,可以使用 revalidateTag 函数。例如,您可以为单个 fetch 调用添加标签:

app/blog/page.tsx
export default async function Page() {
const data = await fetch('https://api.vercel.app/blog', {
next: { tags: ['posts'] },
})
const posts = await data.json()
// ...
}

如果您使用 ORM 或连接到数据库,可以使用 unstable_cache

app/blog/page.tsx
import { unstable_cache } from 'next/cache'
import { db, posts } from '@/lib/db'

const getCachedPosts = unstable_cache(
async () => {
return await db.select().from(posts)
},
['posts'],
{ revalidate: 3600, tags: ['posts'] }
)

export default async function Page() {
const posts = getCachedPosts()
// ...
}

然后您可以在服务器操作路由处理器中使用 revalidateTag

app/actions.ts
'use server'

import { revalidateTag } from 'next/cache'

export async function createPost() {
// 使缓存中标记为 'posts' 的所有数据失效
revalidateTag('posts')
}

使用 res.revalidate() 进行按需验证

对于更精确的重新验证方法,使用 res.revalidate 从 API 路由按需生成新页面。

例如,可以调用 /api/revalidate?secret=<token> 来重新验证给定的博客文章。创建一个只有您的 Next.js 应用知道的密钥令牌。此密钥将用于防止对重新验证 API 路由的未授权访问。

pages/api/revalidate.ts
import type { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
// 检查密钥以确认这是有效请求
if (req.query.secret !== process.env.MY_SECRET_TOKEN) {
return res.status(401).json({ message: 'Invalid token' })
}

try {
// 这应该是实际路径而不是重写的路径
// 例如,对于 "/posts/[id]" 这应该是 "/posts/1"
await res.revalidate('/posts/1')
return res.json({ revalidated: true })
} catch (err) {
// 如果有错误,Next.js 将继续
// 显示最后成功生成的页面
return res.status(500).send('Error revalidating')
}
}

如果您使用按需重新验证,则不需要在 getStaticProps 中指定 revalidate 时间。Next.js 将使用默认值 false(不重新验证),仅在调用 res.revalidate() 时按需重新验证页面。

处理未捕获的异常

如果在尝试重新验证数据时抛出错误,将继续从缓存中提供最后成功生成的数据。在下一个后续请求中,Next.js 将重试重新验证数据。了解更多关于错误处理的信息

如果在处理后台重新生成时 getStaticProps 内部出现错误,或者您手动抛出错误,将继续显示最后成功生成的页面。在下一个后续请求中,Next.js 将重试调用 getStaticProps

pages/blog/[id].tsx
import type { GetStaticProps } from 'next'

interface Post {
id: string
title: string
content: string
}

interface Props {
post: Post
}

export const getStaticProps: GetStaticProps<Props> = async ({
params,
}: {
params: { id: string }
}) => {
// 如果此请求抛出未捕获的错误,Next.js 将
// 不会使当前显示的页面失效,并在
// 下一个请求时重试 getStaticProps。
const res = await fetch(`https://api.vercel.app/blog/${params.id}`)
const post: Post = await res.json()

if (!res.ok) {
// 如果有服务器错误,您可能想要
// 抛出错误而不是返回,这样缓存就不会更新
// 直到下一个成功的请求。
throw new Error(`Failed to fetch posts, received status ${res.status}`)
}

return {
props: { post },
// Next.js 将在请求到达时使缓存失效,
// 最多每 60 秒一次。
revalidate: 60,
}
}

自定义缓存位置

如果您想要将缓存的页面和数据持久化到持久存储,或在 Next.js 应用的多个容器或实例之间共享缓存,可以配置 Next.js 缓存位置。了解更多

故障排除

在本地开发中调试缓存数据

如果您使用 fetch API,可以添加额外的日志记录来了解哪些请求被缓存或未缓存。了解更多关于 logging 选项的信息

next.config.js
module.exports = {
logging: {
fetches: {
fullUrl: true,
},
},
}

验证正确的生产行为

要验证您的页面在生产环境中正确缓存和重新验证,可以通过运行 next build 然后 next start 来本地测试生产 Next.js 服务器。

这将允许您测试 ISR 行为,就像它在生产环境中工作一样。为了进一步调试,在您的 .env 文件中添加以下环境变量:

.env
NEXT_PRIVATE_DEBUG_CACHE=1

这将使 Next.js 服务器控制台记录 ISR 缓存命中和未命中。您可以检查输出以查看在 next build 期间生成了哪些页面,以及页面如何随着路径按需访问而更新。

注意事项

  • ISR 仅在使用 Node.js 运行时(默认)时受支持。
  • 创建静态导出时不支持 ISR。
  • 如果您在静态渲染的路由中有多个 fetch 请求,并且每个都有不同的 revalidate 频率,则将使用最低时间进行 ISR。但是,这些重新验证频率仍将被数据缓存遵守。
  • 如果路由上使用的任何 fetch 请求的 revalidate 时间为 0,或显式的 no-store,则该路由将被动态渲染
  • 中间件不会为按需 ISR 请求执行,这意味着中间件中的任何路径重写或逻辑都不会被应用。确保您正在重新验证确切的路径。例如,/post/1 而不是重写的 /post-1
  • ISR 仅在使用 Node.js 运行时(默认)时受支持。
  • 创建静态导出时不支持 ISR。
  • 中间件不会为按需 ISR 请求执行,这意味着中间件中的任何路径重写或逻辑都不会被应用。确保您正在重新验证确切的路径。例如,/post/1 而不是重写的 /post-1

平台支持

部署选项支持
Node.js 服务器
Docker 容器
静态导出
适配器平台特定

了解在自托管 Next.js 时如何配置 ISR

版本历史

版本变更
v14.1.0自定义 cacheHandler 稳定。
v13.0.0引入 App 路由。
v12.2.0Pages 路由:按需 ISR 稳定
v12.0.0Pages 路由:添加了机器人感知 ISR 回退
v9.5.0Pages 路由:引入稳定 ISR