Next.js 的缓存机制是其性能优化的核心,理解这些缓存策略对于构建高性能应用至关重要。
缓存层次
Next.js 提供多个层次的缓存:
- 构建时缓存:静态生成和 ISR
- 请求时缓存:fetch API 缓存
- CDN 缓存:边缘网络缓存
- 浏览器缓存:客户端缓存
- 数据缓存:React Query、SWR 等
Fetch API 缓存
缓存选项
Next.js 扩展了 fetch API,提供了多种缓存选项。
javascript// 默认缓存(force-cache) async function Page() { const data = await fetch('https://api.example.com/data', { cache: 'force-cache', // 默认 }).then(r => r.json()); return <div>{data.content}</div>; } // 不缓存(no-store) async function Page() { const data = await fetch('https://api.example.com/data', { cache: 'no-store', }).then(r => r.json()); return <div>{data.content}</div>; } // 验证缓存(no-cache) async function Page() { const data = await fetch('https://api.example.com/data', { cache: 'no-cache', // 每次验证缓存 }).then(r => r.json()); return <div>{data.content}</div>; } // 仅当有缓存时使用(only-if-cached) async function Page() { const response = await fetch('https://api.example.com/data', { cache: 'only-if-cached', }); if (!response.ok) { return <div>Loading...</div>; } const data = await response.json(); return <div>{data.content}</div>; }
Next.js 扩展选项
javascript// revalidate:设置重新验证时间(秒) async function Page() { const data = await fetch('https://api.example.com/data', { next: { revalidate: 60, // 每 60 秒重新验证 }, }).then(r => r.json()); return <div>{data.content}</div>; } // tags:用于按需重新验证 async function Page() { const data = await fetch('https://api.example.com/data', { next: { tags: ['posts', 'latest'], }, }).then(r => r.json()); return <div>{data.content}</div>; } // 组合使用 async function Page() { const data = await fetch('https://api.example.com/data', { cache: 'force-cache', next: { revalidate: 3600, tags: ['data'], }, }).then(r => r.json()); return <div>{data.content}</div>; }
按需重新验证
revalidatePath
javascriptimport { revalidatePath } from 'next/cache'; 'use server'; export async function updatePost(postId: string, formData: FormData) { const title = formData.get('title'); const content = formData.get('content'); await db.post.update({ where: { id: postId }, data: { title, content }, }); // 重新验证特定路径 revalidatePath('/posts'); revalidatePath(`/posts/${postId}`); return { success: true }; }
revalidateTag
javascriptimport { revalidateTag } from 'next/cache'; 'use server'; export async function createPost(formData: FormData) { const title = formData.get('title'); const content = formData.get('content'); await db.post.create({ data: { title, content }, }); // 重新验证所有带有 'posts' 标签的缓存 revalidateTag('posts'); return { success: true }; } // 使用标签的数据获取 async function Page() { const posts = await fetch('https://api.example.com/posts', { next: { tags: ['posts'], revalidate: 3600, }, }).then(r => r.json()); return <PostList posts={posts} />; }
静态生成缓存
getStaticProps 缓存
javascriptexport async function getStaticProps() { const data = await fetchData(); return { props: { data }, revalidate: 3600, // ISR:每小时重新生成 }; }
getStaticPaths 缓存
javascriptexport async function getStaticPaths() { const posts = await getAllPosts(); return { paths: posts.map(post => ({ params: { slug: post.slug } })), fallback: 'blocking', // 或 false 或 true }; } export async function getStaticProps({ params }) { const post = await getPostBySlug(params.slug); return { props: { post }, revalidate: 86400, // 每天重新生成 }; }
服务器组件缓存
默认缓存行为
javascript// 默认情况下,fetch 请求会被缓存 async function Page() { const data = await fetch('https://api.example.com/data') .then(r => r.json()); return <div>{data.content}</div>; }
禁用缓存
javascript// 禁用缓存 async function Page() { const data = await fetch('https://api.example.com/data', { cache: 'no-store', }).then(r => r.json()); return <div>{data.content}</div>; }
动态数据获取
javascript// 对于动态数据,禁用缓存 async function UserDashboard({ params }) { const user = await fetch(`https://api.example.com/user/${params.id}`, { cache: 'no-store', }).then(r => r.json()); return <Dashboard user={user} />; }
客户端缓存
React Query 缓存
javascript'use client'; import { useQuery, useQueryClient } from '@tanstack/react-query'; export default function DataComponent() { const queryClient = useQueryClient(); const { data, isLoading } = useQuery({ queryKey: ['data'], queryFn: () => fetch('/api/data').then(r => r.json()), staleTime: 60000, // 数据在 60 秒内被认为是新鲜的 cacheTime: 300000, // 缓存在 5 分钟内有效 }); const handleRefresh = () => { // 手动刷新数据 queryClient.invalidateQueries({ queryKey: ['data'] }); }; if (isLoading) return <div>Loading...</div>; return ( <div> <div>{data.content}</div> <button onClick={handleRefresh}>Refresh</button> </div> ); }
SWR 缓存
javascript'use client'; import useSWR from 'swr'; const fetcher = (url) => fetch(url).then(r => r.json()); export default function DataComponent() { const { data, error, isLoading, mutate } = useSWR( '/api/data', fetcher, { revalidateOnFocus: false, // 窗口聚焦时不重新验证 revalidateOnReconnect: false, // 重新连接时不重新验证 dedupingInterval: 60000, // 60 秒内去重请求 refreshInterval: 0, // 不自动刷新 } ); const handleRefresh = () => { // 手动刷新数据 mutate(); }; if (isLoading) return <div>Loading...</div>; if (error) return <div>Error</div>; return ( <div> <div>{data.content}</div> <button onClick={handleRefresh}>Refresh</button> </div> ); }
CDN 缓存
Vercel CDN
部署到 Vercel 时,静态资源会自动缓存到 CDN。
javascript// next.config.js module.exports = { // 静态资源会被缓存到 Vercel CDN output: 'standalone', };
自定义 CDN 头
javascript// app/api/data/route.js export async function GET() { const data = await fetchData(); return Response.json(data, { headers: { 'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400', 'CDN-Cache-Control': 'public, s-maxage=3600', }, }); }
浏览器缓存
设置缓存头
javascript// app/api/data/route.js export async function GET() { const data = await fetchData(); return Response.json(data, { headers: { // 强缓存:1 小时 'Cache-Control': 'public, max-age=3600, immutable', // 协商缓存:ETag 'ETag': generateETag(data), // 最后修改时间 'Last-Modified': new Date().toUTCString(), }, }); } function generateETag(data) { const crypto = require('crypto'); return crypto.createHash('md5').update(JSON.stringify(data)).digest('hex'); }
条件请求
javascript// app/api/data/route.js export async function GET(request) { const data = await fetchData(); const etag = generateETag(data); // 检查 If-None-Match 头 const ifNoneMatch = request.headers.get('if-none-match'); if (ifNoneMatch === etag) { return new Response(null, { status: 304 }); } return Response.json(data, { headers: { 'ETag': etag, 'Cache-Control': 'public, max-age=3600', }, }); }
高级缓存策略
分层缓存
javascript// 第一层:浏览器缓存 // 第二层:CDN 缓存 // 第三层:Next.js 缓存 // 第四层:数据源 async function Page() { const data = await fetch('https://api.example.com/data', { // Next.js 缓存 cache: 'force-cache', next: { revalidate: 3600, tags: ['data'], }, }).then(r => r.json()); return <div>{data.content}</div>; } // API 路由设置 CDN 缓存 export async function GET() { const data = await fetchData(); return Response.json(data, { headers: { // CDN 缓存 'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400', }, }); }
智能缓存失效
javascript'use server'; import { revalidateTag, revalidatePath } from 'next/cache'; export async function updateData(id: string, formData: FormData) { const data = await updateDataInDB(id, formData); // 根据数据类型选择失效策略 if (data.type === 'post') { revalidateTag('posts'); revalidatePath('/posts'); } else if (data.type === 'user') { revalidateTag('users'); revalidatePath('/users'); } return { success: true }; }
缓存预热
javascript// app/api/warmup/route.js export async function GET() { const urls = [ 'https://example.com/api/data', 'https://example.com/api/posts', 'https://example.com/api/users', ]; // 并行预热缓存 await Promise.all( urls.map(url => fetch(url)) ); return Response.json({ message: 'Cache warmed up' }); }
实际应用场景
1. 博客文章缓存
javascript// app/blog/[slug]/page.js async function BlogPost({ params }) { const post = await fetch(`https://api.example.com/posts/${params.slug}`, { next: { revalidate: 86400, // 每天重新验证 tags: [`post-${params.slug}`, 'posts'], }, }).then(r => r.json()); return <PostContent post={post} />; } // 更新文章时重新验证 'use server'; import { revalidateTag } from 'next/cache'; export async function updatePost(postId: string, formData: FormData) { const post = await updatePostInDB(postId, formData); revalidateTag(`post-${post.slug}`); revalidateTag('posts'); return { success: true }; }
2. 电商产品缓存
javascript// app/products/[id]/page.js async function ProductPage({ params }) { const product = await fetch(`https://api.example.com/products/${params.id}`, { next: { revalidate: 3600, // 每小时重新验证 tags: [`product-${params.id}`, 'products'], }, }).then(r => r.json()); return <ProductDetails product={product} />; } // 产品列表页面 async function ProductListPage() { const products = await fetch('https://api.example.com/products', { next: { revalidate: 600, // 每 10 分钟重新验证 tags: ['products'], }, }).then(r => r.json()); return <ProductList products={products} />; }
3. 用户数据缓存
javascript// app/dashboard/page.js async function DashboardPage() { const session = await auth(); // 用户数据不缓存(动态数据) const user = await fetch(`https://api.example.com/user/${session.user.id}`, { cache: 'no-store', }).then(r => r.json()); // 统计数据可以缓存 const stats = await fetch(`https://api.example.com/stats/${session.user.id}`, { next: { revalidate: 300, // 每 5 分钟重新验证 tags: [`stats-${session.user.id}`], }, }).then(r => r.json()); return <Dashboard user={user} stats={stats} />; }
4. 实时数据缓存
javascript// app/live/page.js async function LivePage() { // 实时数据不缓存 const liveData = await fetch('https://api.example.com/live', { cache: 'no-store', }).then(r => r.json()); return <LiveData data={liveData} />; } // 使用 SWR 进行客户端轮询 'use client'; import useSWR from 'swr'; export default function LiveDataComponent() { const { data } = useSWR( '/api/live', fetcher, { refreshInterval: 5000, // 每 5 秒刷新 revalidateOnFocus: true, } ); return <div>{data?.content}</div>; }
最佳实践
1. 根据数据类型选择缓存策略
javascript// ✅ 好的做法:静态内容使用长时间缓存 async function Page() { const data = await fetch('https://api.example.com/static-data', { next: { revalidate: 86400, // 1 天 }, }).then(r => r.json()); return <div>{data.content}</div>; } // ✅ 好的做法:动态内容禁用缓存 async function Page() { const data = await fetch('https://api.example.com/dynamic-data', { cache: 'no-store', }).then(r => r.json()); return <div>{data.content}</div>; } // ❌ 不好的做法:所有数据使用相同缓存策略 async function Page() { const data = await fetch('https://api.example.com/data', { next: { revalidate: 3600, }, }).then(r => r.json()); return <div>{data.content}</div>; }
2. 使用标签进行精确的缓存失效
javascript// ✅ 好的做法:使用标签 async function Page() { const data = await fetch('https://api.example.com/data', { next: { tags: ['posts', 'latest'], }, }).then(r => r.json()); return <div>{data.content}</div>; } // 更新时只失效相关标签 revalidateTag('posts'); // ❌ 不好的做法:失效整个路径 revalidatePath('/');
3. 合理设置重新验证时间
javascript// ✅ 好的做法:根据更新频率设置 async function Page() { const data = await fetch('https://api.example.com/data', { next: { revalidate: 3600, // 1 小时 }, }).then(r => r.json()); return <div>{data.content}</div>; } // ❌ 不好的做法:设置过短的重新验证时间 async function Page() { const data = await fetch('https://api.example.com/data', { next: { revalidate: 10, // 10 秒(太短) }, }).then(r => r.json()); return <div>{data.content}</div>; }
4. 监控缓存命中率
javascript// lib/cacheMonitor.js export function logCacheHit(key: string, hit: boolean) { console.log(`Cache ${hit ? 'hit' : 'miss'}: ${key}`); // 发送到监控服务 if (typeof window !== 'undefined') { fetch('/api/cache-log', { method: 'POST', body: JSON.stringify({ key, hit }), }); } } // 使用 async function Page() { const key = 'data'; const data = await fetch('https://api.example.com/data', { next: { tags: [key], }, }).then(r => r.json()); logCacheHit(key, true); return <div>{data.content}</div>; }
通过合理使用 Next.js 的缓存机制,可以显著提升应用性能,减少服务器负载,提供更好的用户体验。