Next.js caching mechanism is the core of its performance optimization. Understanding these caching strategies is crucial for building high-performance applications.
Caching Layers
Next.js provides multiple layers of caching:
- Build-time caching: Static generation and ISR
- Request-time caching: fetch API caching
- CDN caching: Edge network caching
- Browser caching: Client-side caching
- Data caching: React Query, SWR, etc.
Fetch API Caching
Cache Options
Next.js extends the fetch API with various caching options.
javascript// Default caching (force-cache) async function Page() { const data = await fetch('https://api.example.com/data', { cache: 'force-cache', // Default }).then(r => r.json()); return <div>{data.content}</div>; } // No caching (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>; } // Validate cache (no-cache) async function Page() { const data = await fetch('https://api.example.com/data', { cache: 'no-cache', // Validate cache every time }).then(r => r.json()); return <div>{data.content}</div>; } // Only use if cached (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 Extended Options
javascript// revalidate: Set revalidation time (seconds) async function Page() { const data = await fetch('https://api.example.com/data', { next: { revalidate: 60, // Revalidate every 60 seconds }, }).then(r => r.json()); return <div>{data.content}</div>; } // tags: For on-demand revalidation 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>; } // Combine usage 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>; }
On-demand Revalidation
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 }, }); // Revalidate specific path 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 }, }); // Revalidate all caches with 'posts' tag revalidateTag('posts'); return { success: true }; } // Data fetching with tags 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} />; }
Static Generation Caching
getStaticProps Caching
javascriptexport async function getStaticProps() { const data = await fetchData(); return { props: { data }, revalidate: 3600, // ISR: Regenerate every hour }; }
getStaticPaths Caching
javascriptexport async function getStaticPaths() { const posts = await getAllPosts(); return { paths: posts.map(post => ({ params: { slug: post.slug } })), fallback: 'blocking', // Or false or true }; } export async function getStaticProps({ params }) { const post = await getPostBySlug(params.slug); return { props: { post }, revalidate: 86400, // Regenerate every day }; }
Server Component Caching
Default Cache Behavior
javascript// By default, fetch requests are cached async function Page() { const data = await fetch('https://api.example.com/data') .then(r => r.json()); return <div>{data.content}</div>; }
Disable Caching
javascript// Disable caching async function Page() { const data = await fetch('https://api.example.com/data', { cache: 'no-store', }).then(r => r.json()); return <div>{data.content}</div>; }
Dynamic Data Fetching
javascript// For dynamic data, disable caching 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} />; }
Client-side Caching
React Query Caching
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, // Data is considered fresh for 60 seconds cacheTime: 300000, // Cache is valid for 5 minutes }); const handleRefresh = () => { // Manually refresh data queryClient.invalidateQueries({ queryKey: ['data'] }); }; if (isLoading) return <div>Loading...</div>; return ( <div> <div>{data.content}</div> <button onClick={handleRefresh}>Refresh</button> </div> ); }
SWR Caching
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, // Don't revalidate on window focus revalidateOnReconnect: false, // Don't revalidate on reconnect dedupingInterval: 60000, // Deduplicate requests within 60 seconds refreshInterval: 0, // Don't auto-refresh } ); const handleRefresh = () => { // Manually refresh data 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 Caching
Vercel CDN
When deploying to Vercel, static assets are automatically cached on CDN.
javascript// next.config.js module.exports = { // Static assets are cached on Vercel CDN output: 'standalone', };
Custom CDN Headers
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', }, }); }
Browser Caching
Set Cache Headers
javascript// app/api/data/route.js export async function GET() { const data = await fetchData(); return Response.json(data, { headers: { // Strong cache: 1 hour 'Cache-Control': 'public, max-age=3600, immutable', // Negotiation cache: ETag 'ETag': generateETag(data), // Last modified time 'Last-Modified': new Date().toUTCString(), }, }); } function generateETag(data) { const crypto = require('crypto'); return crypto.createHash('md5').update(JSON.stringify(data)).digest('hex'); }
Conditional Requests
javascript// app/api/data/route.js export async function GET(request) { const data = await fetchData(); const etag = generateETag(data); // Check If-None-Match header 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', }, }); }
Advanced Caching Strategies
Layered Caching
javascript// Layer 1: Browser cache // Layer 2: CDN cache // Layer 3: Next.js cache // Layer 4: Data source async function Page() { const data = await fetch('https://api.example.com/data', { // Next.js cache cache: 'force-cache', next: { revalidate: 3600, tags: ['data'], }, }).then(r => r.json()); return <div>{data.content}</div>; } // API route sets CDN cache export async function GET() { const data = await fetchData(); return Response.json(data, { headers: { // CDN cache 'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400', }, }); }
Smart Cache Invalidation
javascript'use server'; import { revalidateTag, revalidatePath } from 'next/cache'; export async function updateData(id: string, formData: FormData) { const data = await updateDataInDB(id, formData); // Choose invalidation strategy based on data type if (data.type === 'post') { revalidateTag('posts'); revalidatePath('/posts'); } else if (data.type === 'user') { revalidateTag('users'); revalidatePath('/users'); } return { success: true }; }
Cache Warming
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', ]; // Warm up cache in parallel await Promise.all( urls.map(url => fetch(url)) ); return Response.json({ message: 'Cache warmed up' }); }
Real-world Use Cases
1. Blog Post Caching
javascript// app/blog/[slug]/page.js async function BlogPost({ params }) { const post = await fetch(`https://api.example.com/posts/${params.slug}`, { next: { revalidate: 86400, // Revalidate every day tags: [`post-${params.slug}`, 'posts'], }, }).then(r => r.json()); return <PostContent post={post} />; } // Revalidate when updating 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. E-commerce Product Caching
javascript// app/products/[id]/page.js async function ProductPage({ params }) { const product = await fetch(`https://api.example.com/products/${params.id}`, { next: { revalidate: 3600, // Revalidate every hour tags: [`product-${params.id}`, 'products'], }, }).then(r => r.json()); return <ProductDetails product={product} />; } // Product list page async function ProductListPage() { const products = await fetch('https://api.example.com/products', { next: { revalidate: 600, // Revalidate every 10 minutes tags: ['products'], }, }).then(r => r.json()); return <ProductList products={products} />; }
3. User Data Caching
javascript// app/dashboard/page.js async function DashboardPage() { const session = await auth(); // User data not cached (dynamic data) const user = await fetch(`https://api.example.com/user/${session.user.id}`, { cache: 'no-store', }).then(r => r.json()); // Stats data can be cached const stats = await fetch(`https://api.example.com/stats/${session.user.id}`, { next: { revalidate: 300, // Revalidate every 5 minutes tags: [`stats-${session.user.id}`], }, }).then(r => r.json()); return <Dashboard user={user} stats={stats} />; }
4. Real-time Data Caching
javascript// app/live/page.js async function LivePage() { // Real-time data not cached const liveData = await fetch('https://api.example.com/live', { cache: 'no-store', }).then(r => r.json()); return <LiveData data={liveData} />; } // Use SWR for client-side polling 'use client'; import useSWR from 'swr'; export default function LiveDataComponent() { const { data } = useSWR( '/api/live', fetcher, { refreshInterval: 5000, // Refresh every 5 seconds revalidateOnFocus: true, } ); return <div>{data?.content}</div>; }
Best Practices
1. Choose Caching Strategy Based on Data Type
javascript// ✅ Good practice: Static content uses long cache async function Page() { const data = await fetch('https://api.example.com/static-data', { next: { revalidate: 86400, // 1 day }, }).then(r => r.json()); return <div>{data.content}</div>; } // ✅ Good practice: Dynamic content disables cache 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>; } // ❌ Bad practice: All data uses same caching strategy 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. Use Tags for Precise Cache Invalidation
javascript// ✅ Good practice: Use 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>; } // Invalidate only related tags when updating revalidateTag('posts'); // ❌ Bad practice: Invalidate entire path revalidatePath('/');
3. Set Reasonable Revalidation Time
javascript// ✅ Good practice: Set based on update frequency async function Page() { const data = await fetch('https://api.example.com/data', { next: { revalidate: 3600, // 1 hour }, }).then(r => r.json()); return <div>{data.content}</div>; } // ❌ Bad practice: Set too short revalidation time async function Page() { const data = await fetch('https://api.example.com/data', { next: { revalidate: 10, // 10 seconds (too short) }, }).then(r => r.json()); return <div>{data.content}</div>; }
4. Monitor Cache Hit Rate
javascript// lib/cacheMonitor.js export function logCacheHit(key: string, hit: boolean) { console.log(`Cache ${hit ? 'hit' : 'miss'}: ${key}`); // Send to monitoring service if (typeof window !== 'undefined') { fetch('/api/cache-log', { method: 'POST', body: JSON.stringify({ key, hit }), }); } } // Usage 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>; }
By properly using Next.js caching mechanisms, you can significantly improve application performance, reduce server load, and provide better user experience.