Originally published on tamiz.pro.
Next.js 13's App Router introduces powerful caching capabilities that dramatically improve performance without sacrificing developer experience. This deep-dive explores advanced patterns that combine edge caching, server-side strategies, and distributed caching systems.
Edge Caching with App Router
The App Router enables edge caching through its built-in fetch cache. For example:
// app/data/page.js
async function getData() {
const res = await fetch('https://api.example.com/data', {
cache: 'force-cache' // Leverages edge cache
});
return res.json();
}
This pattern uses the global edge cache for static assets and API responses. The cache option supports:
-
force-cache: Always return cached response -
no-cache: Bypass cache for fresh response -
default: Use browser heuristic
Edge caching reduces latency but requires careful validation when freshness matters.
Server-Side Caching Strategies
For dynamic data, Next.js provides tag-based caching:
// app/api/data/route.js
import { revalidateTag } from 'next/cache';
export async function GET() {
const data = await fetch('https://api.example.com/data', {
cache: 'force-cache',
next: { tags: ['user-123'] } } // Cache tag association
});
return Response.json(data);
}
This allows granular cache invalidation:
// Invalidate single tag
revalidateTag('user-123');
// Invalidate multiple tags
revalidateTag(['user-123', 'user-456']);
Cache Revalidation Patterns
- Scheduled Revalidation:
// Revalidate every 60 seconds
export const revalidate = 60;
- Manual Revalidation:
// Triggered via API route
export async function GET() {
await revalidateTag('article-123');
return Response.json({ revalidated: true });
}
Combining Edge and Server Caching
Advanced applications often use layered caching:
- Edge Layer: Caches static assets and public data
- Server Layer: Manages user-specific data with tags
- Distributed Layer: Redis for cross-instance consistency
// Example layered caching
function getCachedData(key) {
const edgeResult = await getFromEdgeCache(key);
if (edgeResult) return edgeResult;
const redisResult = await redis.get(key);
if (redisResult) return redisResult;
const freshData = await fetchData();
await redis.setex(key, 3600, freshData);
return freshData;
}
Distributed Caching with Redis
For multi-instance deployments, integrate Redis via ioredis:
// Using ioredis
const Redis = require('ioredis');
const redis = new Redis();
async function getFromCache(key) {
const cached = await redis.get(key);
return cached ? JSON.parse(cached) : null;
}
async function setInCache(key, data, ttl = 3600) {
await redis.setex(key, ttl, JSON.stringify(data));
}
This pattern ensures consistency across edge and server nodes but introduces Redis dependency management challenges.
Cache Tagging Best Practices
-
Granular Tags: Use specific tags like
user-123-profileinstead of genericuser - Tag Hierarchies: Combine tags strategically
{ tags: ['article-123', 'author-456', 'category-tech'] }
- Batch Invalidation: Group related tags for bulk invalidation
Performance Optimization Techniques
- Stale-While-Revalidate:
// Serve cached content while updating in background
const res = fetch(...);
res.headers.set('Cache-Control', 'stale-while-revalidate');
- Cache Partitioning: Use separate Redis namespaces for different data types:
const ARTICLE_PREFIX = 'article:';
const USER_PREFIX = 'user:';
- Conditional Caching:
if (isAuthenticated) {
return fetch(...);
} else {
return fetch(..., { cache: 'force-cache' });
}
Monitoring and Debugging
- Add logging middleware:
export async function middleware(request) {
console.log(`Cache hit: ${request.cache}`);
return NextResponse.next();
}
-
Use Vercel's analytics:
- Edge cache hit rate metrics
- Server component rendering duration
Test cache behavior:
// Simulate cache invalidation
curl -X POST https://your-app/invalidation?tag=user-123
By combining these patterns, you can build Next.js applications that maintain sub-100ms load times at scale while ensuring data consistency through intelligent cache management. The choice between edge caching, Redis, or hybrid approaches depends on your specific latency and consistency requirements.
Top comments (1)
How do you handle cache invalidation with Redis in a high-traffic Next.js app? I'd love to swap ideas on this, following for more insights on server-side caching.