Next.js增量静态再生ISR #
一、ISR基础 #
1.1 什么是ISR #
增量静态再生允许在构建后更新静态页面:
text
构建时生成 → 定期更新 → 保持静态优势
1.2 ISR优点 #
- 静态页面速度
- 数据保持更新
- 服务器压力小
- SEO友好
1.3 ISR缺点 #
- 数据有延迟
- 首次更新较慢
1.4 适用场景 #
- 博客文章
- 产品页面
- 新闻列表
- 分类目录
二、基于时间的重新验证 #
2.1 页面级配置 #
tsx
export const revalidate = 60
export default async function Page() {
const data = await fetch('https://api.example.com/data')
return <div>{data}</div>
}
2.2 fetch级配置 #
tsx
export default async function Page() {
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 60 },
})
return <div>{data}</div>
}
2.3 工作原理 #
text
请求1 → 返回缓存页面 → 后台触发更新
请求2(60秒内)→ 返回缓存页面
请求3(60秒后)→ 返回新页面
2.4 时间选择 #
| 时间 | 适用场景 |
|---|---|
| 60秒 | 新闻、动态 |
| 3600秒 | 博客、产品 |
| 86400秒 | 静态内容 |
三、按需重新验证 #
3.1 基于标签 #
设置标签
tsx
export default async function Page() {
const posts = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] },
})
return <div>{posts}</div>
}
触发重新验证
tsx
'use server'
import { revalidateTag } from 'next/cache'
export async function createPost(data: PostData) {
await db.post.create({ data })
revalidateTag('posts')
}
3.2 基于路径 #
tsx
'use server'
import { revalidatePath } from 'next/cache'
export async function updatePost(slug: string, data: PostData) {
await db.post.update({
where: { slug },
data,
})
revalidatePath('/blog')
revalidatePath(`/blog/${slug}`)
}
3.3 API Route触发 #
tsx
import { revalidateTag, revalidatePath } from 'next/cache'
import { NextResponse } from 'next/server'
export async function POST(request: Request) {
const body = await request.json()
if (body.tag) {
revalidateTag(body.tag)
}
if (body.path) {
revalidatePath(body.path)
}
return NextResponse.json({ revalidated: true })
}
3.4 Webhook触发 #
tsx
import { revalidateTag } from 'next/cache'
import { NextResponse } from 'next/server'
export async function POST(request: Request) {
const body = await request.json()
if (body.model === 'post') {
revalidateTag('posts')
}
if (body.model === 'product') {
revalidateTag('products')
}
return NextResponse.json({ received: true })
}
四、缓存标签管理 #
4.1 标签组织 #
tsx
const CACHE_TAGS = {
posts: 'posts',
post: (slug: string) => `post:${slug}`,
products: 'products',
product: (id: string) => `product:${id}`,
categories: 'categories',
} as const
export default async function Page() {
const posts = await fetch('/api/posts', {
next: { tags: [CACHE_TAGS.posts] },
})
return <div>{posts}</div>
}
4.2 多标签 #
tsx
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const post = await fetch(`/api/posts/${slug}`, {
next: {
tags: [CACHE_TAGS.posts, CACHE_TAGS.post(slug)],
},
})
return <article>{post}</article>
}
4.3 精确更新 #
tsx
'use server'
import { revalidateTag } from 'next/cache'
export async function updatePost(slug: string, data: PostData) {
await db.post.update({
where: { slug },
data,
})
revalidateTag(`post:${slug}`)
}
五、ISR与动态路由 #
5.1 generateStaticParams #
tsx
export const revalidate = 3600
export async function generateStaticParams() {
const posts = await getPosts()
return posts.map(post => ({
slug: post.slug,
}))
}
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const post = await getPost(slug)
return <article>{post.content}</article>
}
5.2 动态参数 #
tsx
export const dynamicParams = true
export const revalidate = 3600
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const post = await getPost(slug)
return <article>{post.content}</article>
}
六、ISR最佳实践 #
6.1 分层缓存 #
tsx
export default async function Page() {
const staticContent = await fetch('/api/static', {
cache: 'force-cache',
})
const semiStaticContent = await fetch('/api/semi-static', {
next: { revalidate: 3600 },
})
const dynamicContent = await fetch('/api/dynamic', {
cache: 'no-store',
})
return (
<div>
<StaticSection data={staticContent} />
<SemiStaticSection data={semiStaticContent} />
<DynamicSection data={dynamicContent} />
</div>
)
}
6.2 条件重新验证 #
tsx
'use server'
import { revalidatePath } from 'next/cache'
export async function publishPost(slug: string) {
const post = await db.post.update({
where: { slug },
data: { published: true },
})
if (post.published) {
revalidatePath('/blog')
revalidatePath(`/blog/${slug}`)
}
}
6.3 批量更新 #
tsx
'use server'
import { revalidateTag } from 'next/cache'
export async function bulkUpdatePosts(posts: PostData[]) {
await db.post.createMany({ data: posts })
revalidateTag('posts')
}
七、ISR调试 #
7.1 缓存头 #
查看响应头:
text
x-nextjs-cache: HIT # 缓存命中
x-nextjs-cache: MISS # 缓存未命中
x-nextjs-cache: STALE # 缓存过期
7.2 开发模式 #
开发模式下ISR默认禁用,可以通过以下方式测试:
tsx
export default async function Page() {
console.log('Page rendered at:', new Date().toISOString())
const data = await fetch('/api/data', {
next: { revalidate: 60 },
})
return <div>{data}</div>
}
7.3 预览模式 #
tsx
import { draftMode } from 'next/headers'
export default async function Page() {
const { isEnabled } = await draftMode()
const data = await fetch('/api/data', {
cache: isEnabled ? 'no-store' : 'force-cache',
})
return <div>{data}</div>
}
八、实战案例 #
8.1 博客系统 #
tsx
export const revalidate = 3600
export async function generateStaticParams() {
const posts = await getPosts()
return posts.map(post => ({ slug: post.slug }))
}
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const post = await getPost(slug)
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
)
}
8.2 电商产品 #
tsx
export const revalidate = 300
export async function generateStaticParams() {
const products = await getProducts()
return products.map(product => ({
category: product.category,
id: product.id,
}))
}
export default async function ProductPage({ params }: { params: Promise<{ category: string; id: string }> }) {
const { category, id } = await params
const product = await getProduct(category, id)
return (
<div>
<h1>{product.name}</h1>
<p>价格: ¥{product.price}</p>
</div>
)
}
九、总结 #
ISR要点:
| 要点 | 说明 |
|---|---|
| 时间验证 | revalidate配置 |
| 标签验证 | revalidateTag |
| 路径验证 | revalidatePath |
| 缓存标签 | tags配置 |
| 动态路由 | generateStaticParams |
下一步,让我们学习客户端渲染!
最后更新:2026-03-28