关于 nextjs-caching
面向配置缓存与重新验证的 Next.js 开发者参考技能,介绍各缓存层、fetch 策略和按需失效,并提供排查缓存行为的示例。
主要功能
- 01Next.js 的四种缓存层
- 02fetch 缓存与重新验证选项
- 03按时间和标签失效
- 04静态生成与 ISR 指引
- 05缓存调试方法
适用场景
- 01为 fetch 调用选择缓存策略
- 02数据变更后重新验证内容
- 03配置静态页面与 ISR
- 04排查过期或异常缓存数据
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ josiahsiegel/ claude-plugin-marketplace --skill nextjs-caching安装后在 Claude Code 中输入/nextjs-caching确认已加载。
详细介绍
快速参考
| 缓存层 | 位置 | 时长 | 用途 |
|---|---|---|---|
| 请求记忆化 | 服务端 | 每次请求 | 对相同的请求进行去重 |
| 数据缓存 | 服务端 | 持久 | 存储 fetch 结果 |
| 完整路由缓存 | 服务端 | 持久 | 预渲染的 HTML/RSC |
| 路由器缓存 | 客户端 | 会话期间 | 客户端导航 |
Next.js 16:'use cache' 指令
| 指令 | 范围 | 使用场景 |
|---|---|---|
'use cache' | 默认缓存 | 通用缓存 |
'use cache: remote' | CDN/边缘缓存 | 静态共享内容 |
'use cache: private' | 按用户缓存 | 用户专属数据 |
| 缓存函数 | 代码 | 用途 |
|---|---|---|
cacheLife('hours') | 时长 | 设置缓存生命周期 |
cacheTag('posts') | 标记 | 启用定向重新验证 |
Fetch 选项
| Fetch 选项 | 代码 | 效果 |
|---|---|---|
| 默认 | fetch(url) | 无限期缓存 |
| 不缓存 | { cache: 'no-store' } | 始终获取最新内容 |
| 重新验证 | { next: { revalidate: 60 } } | 60 秒后过期 |
| 标签 | { next: { tags: ['posts'] } } | 添加标签以便失效 |
| 重新验证 | 代码 | 使用场景 |
|---|---|---|
revalidatePath('/posts') | 路径 | 修改后 |
revalidateTag('posts') | 标签 | 使带标签的数据失效 |
router.refresh() | 客户端 | 刷新当前路由 |
何时使用此技能
用于缓存和重新验证:
- 了解 Next.js 的缓存行为
- 配置 fetch 缓存策略
- 设置增量静态再生成(ISR)
- 实现按需重新验证
- 调试缓存问题
相关技能:
- 关于数据获取:请参阅
nextjs-data-fetching - 关于 Server Actions:请参阅
nextjs-server-actions - 关于静态生成:请参阅
nextjs-deployment
Next.js 缓存
缓存概览
Next.js 有四种缓存机制:
| 机制 | 内容 | 位置 | 用途 | 时长 |
|---|---|---|---|---|
| 请求记忆化 | 函数的返回值 | 服务端 | 避免组件树中的重复请求 | 每次请求 |
| 数据缓存 | 数据 | 服务端 | 在多个请求和部署之间存储数据 | 持久 |
| 完整路由缓存 | HTML 和 RSC 负载 | 服务端 | 降低渲染成本 | 持久 |
| 路由器缓存 | RSC 负载 | 客户端 | 减少导航时的服务端请求 | 会话期间 |
请求记忆化
自动去重
// This is automatically memoized - only one fetch happens
async function getUser(id: string) {
const res = await fetch(`https://api.example.com/users/${id}`);
return res.json();
}
// Layout uses getUser
export default async function Layout({ children }: { children: React.ReactNode }) {
const user = await getUser('1'); // First call - fetches
return <div>{children}</div>;
}
// Page also uses getUser
export default async function Page() {
const user = await getUser('1'); // Second call - uses cached result
return <div>{user.name}</div>;
}
使用 cache() 手动记忆化
import { cache } from 'react';
// For non-fetch functions like database queries
export const getUser = cache(async (id: string) => {
return db.users.findUnique({ where: { id } });
});
// Both will use the same cached result
const user1 = await getUser('1');
const user2 = await getUser('1');
数据缓存
使用 fetch() 缓存
// Cached indefinitely (default)
const data = await fetch('https://api.example.com/data');
// Opt out of caching
const data = await fetch('https://api.example.com/data', {
cache: 'no-store',
});
// Time-based revalidation
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 }, // Revalidate every hour
});
// Tag-based caching
const data = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] },
});
对非 fetch 操作使用 unstable_cache
import { unstable_cache } from 'next/cache';
const getCachedUser = unstable_cache(
async (userId: string) => {
return db.users.findUnique({ where: { id: userId } });
},
['user'], // Cache key parts
{
revalidate: 3600, // 1 hour
tags: ['users'],
}
);
export default async function UserPage({ params }: { params: { id: string } }) {
const user = await getCachedUser(params.id);
return <div>{user?.name}</div>;
}
路由段配置
// Opt entire route out of caching
export const dynamic = 'force-dynamic';
// Set revalidation for entire route
export const revalidate = 60; // seconds
// Disable cache for all fetches in route
export const fetchCache = 'force-no-store';
// Force static generation
export const dynamic = 'force-static';
重新验证
基于时间的重新验证
// Fetch with revalidation
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 60 }, // Revalidate after 60 seconds
});
// Route-level revalidation
export const revalidate = 60;
按需重新验证
// app/actions.ts
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
// Revalidate a specific path
export async function updatePost(id: string, data: FormData) {
await db.posts.update({ where: { id }, data: { /* ... */ } });
// Revalidate the specific post page
revalidatePath(`/posts/${id}`);
// Revalidate the posts list
revalidatePath('/posts');
}
// Revalidate by tag
export async function createPost(data: FormData) {
await db.posts.create({ data: { /* ... */ } });
// All fetches with 'posts' tag will be revalidated
revalidateTag('posts');
}
重新验证 API 路由
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
const secret = request.headers.get('x-revalidate-secret');
if (secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json({ error: 'Invalid secret' }, { status: 401 });
}
const body = await request.json();
if (body.tag) {
revalidateTag(body.tag);
}
if (body.path) {
revalidatePath(body.path);
}
return NextResponse.json({ revalidated: true, now: Date.now() });
}
基于标签的缓存
// Fetch with tags
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { tags: ['posts', 'content'] },
});
return res.json();
}
async function getPost(id: string) {
const res = await fetch(`https://api.example.com/posts/${id}`, {
next: { tags: ['posts', `post-${id}`] },
});
return res.json();
}
// Revalidate all posts
revalidateTag('posts');
// Revalidate specific post
revalidateTag('post-123');
// Revalidate all content
revalidateTag('content');
完整路由缓存
静态路由
// Automatically cached at build time
export default async function StaticPage() {
const data = await getData();
return <div>{data}</div>;
}
// Force static
export const dynamic = 'force-static';
使用 generateStaticParams 的动态路由
// app/posts/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await db.posts.findMany({ select: { slug: true } });
return posts.map((post) => ({ slug: post.slug }));
}
// Pages are pre-rendered at build time
export default async function PostPage({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
return <article>{post.content}</article>;
}
选择退出
// Dynamic route - not cached
export const dynamic = 'force-dynamic';
// Or use dynamic functions
import { headers, cookies } from 'next/headers';
export default async function Page() {
const headersList = await headers(); // Makes route dynamic
return <div>...</div>;
}
路由器缓存(客户端)
预取
import Link from 'next/link';
// Prefetch on hover (default)
<Link href="/about">About</Link>
// Disable prefetching
<Link href="/dashboard" prefetch={false}>Dashboard</Link>
使路由器缓存失效
'use client';
import { useRouter } from 'next/navigation';
export function RefreshButton() {
const router = useRouter();
const handleRefresh = () => {
// Refresh current route and invalidate cache
router.refresh();
};
return <button onClick={handleRefresh}>Refresh</button>;
}
revalidatePath 和路由器缓存
// Server Action
'use server';
import { revalidatePath } from 'next/cache';
export async function updateData() {
// Update data in database
await db.data.update({ /* ... */ });
// This also clears the Router Cache for this path
revalidatePath('/data');
}
缓存标头
路由处理程序缓存
// app/api/data/route.ts
import { NextResponse } from 'next/server';
export async function GET() {
const data = await fetchData();
return NextResponse.json(data, {
headers: {
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
},
});
}
动态路由中的 generateStaticParams
// Generate some pages statically, allow others dynamically
export async function generateStaticParams() {
// Only pre-render the top 100 posts
const posts = await db.posts.findMany({ take: 100 });
return posts.map((post) => ({ slug: post.slug }));
}
// Allow dynamic rendering for posts not in generateStaticParams
export const dynamicParams = true; // default
// Or 404 for unknown slugs
export const dynamicParams = false;
缓存策略
先返回过期内容,再重新验证模式
// Serve cached content while revalidating in background
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 60 },
});
// First request after 60s: serves stale, triggers revalidation
// Subsequent requests: serve fresh data
缓存优先策略
// Cache indefinitely, revalidate on-demand
const data = await fetch('https://api.example.com/static-data');
// Use revalidateTag('static-data') when data changes
网络优先策略
// Always fetch fresh data
const data = await fetch('https://api.example.com/realtime', {
cache: 'no-store',
});
调试缓存
缓存状态标头
// Enable cache debugging
// next.config.js
module.exports = {
logging: {
fetches: {
fullUrl: true,
},
},
};
检查缓存命中/未命中
// In development, check terminal for cache status
// HIT - served from cache
// MISS - fetched from origin
// STALE - served stale, revalidating
最佳实践
| 实践 | 描述 |
|---|---|
| 使用标签关联相关数据 | 使用标签对相关的 fetch 进行分组 |
| 尽量减少重新验证 | 仅重新验证发生变化的内容 |
| 优先使用静态生成 | 尽可能使用 generateStaticParams |
| 缓存数据库查询 | 对非 fetch 操作使用 unstable_cache |
| 设置合适的 TTL | 平衡内容新鲜度与性能 |
| 对内容使用 ISR | 内容型网站使用基于时间的重新验证 |
评论
支持 Markdown