How Our Caching Strategy Works
Deep dive into the framework's intelligent caching system for optimal performance.
Overview
Strapi-NextGen Framework implements a multi-layered caching strategy that combines:
- ✅ Next.js ISR - Incremental Static Regeneration
- ✅ Cache Tags - Granular invalidation
- ✅ On-Demand Revalidation - Instant updates
- ✅ Automatic Cache Keys - No manual configuration
The Caching Hierarchy
┌─────────────────────────────────────────┐
│ 1. Next.js Data Cache (ISR) │
│ • Time-based: revalidate: 3600 │
│ • Tag-based: tags: ['articles'] │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 2. Framework Cache Tags │
│ • Automatic generation │
│ • Content-type based │
│ • Granular invalidation │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 3. GraphQL Response Cache │
│ • Query-level caching │
│ • Strapi CDN integration │
└─────────────────────────────────────────┘How It Works
1. Automatic Cache Tag Generation
Every Strapi query automatically gets cache tags:
// You write:
const data = await strapi.getPage('home', GetHomePageDocument);
// Framework generates:
// - Cache tag: 'page-home'
// - Revalidation: 3600 seconds (1 hour)Tag Generation Rules:
| Query Type | Generated Tag | Example |
|---|---|---|
getPage(slug) | page-{slug} | page-home |
getCollection(type) | {type} | articles |
getGlobal(name) | global-{name} | global-header |
rawQuery() | Manual or auto | Based on query |
2. Time-Based Revalidation (ISR)
Default behavior without webhooks:
// Cached for 1 hour, then regenerated
const data = await strapi.getPage('home', GetHomePageDocument);
// Next request after 1 hour triggers background regenerationISR Flow:
-
First Request (Cold Cache)
- Fetches from Strapi
- Generates static page
- Caches for 1 hour
-
Subsequent Requests (Warm Cache)
- Serves cached version
- No Strapi request
- Instant response
-
After Revalidation Period
- Serves stale cache
- Regenerates in background
- Next request gets fresh data
3. Tag-Based Invalidation
Instant updates with webhooks:
// Content published in Strapi
// → Webhook triggered
// → revalidateTag('articles')
// → All 'articles' cache invalidated
// → Next request fetches fresh dataSelective Invalidation:
// Invalidate only articles
revalidateTag('articles');
// Invalidate specific article
revalidateTag('article-my-slug');
// Invalidate global data
revalidateTag('global');Cache Configuration
Default Configuration
// Framework defaults
{
revalidate: 3600, // 1 hour
tags: ['auto-generated'], // Based on query
next: {
revalidate: 3600,
},
}Custom Per-Query Configuration
import { unstable_cache } from 'next/cache';
export const getArticles = unstable_cache(
async () => {
return await strapi.getCollection('articles', GetArticlesDocument);
},
['articles-list'], // Cache key
{
revalidate: 1800, // 30 minutes
tags: ['articles', 'blog'], // Multiple tags
}
);Custom Per-Route Configuration
// app/blog/page.tsx
export const revalidate = 600; // 10 minutes
export default async function BlogPage() {
const data = await strapi.getCollection('articles', GetArticlesDocument);
// ...
}Cache Invalidation Strategies
1. On-Demand (Webhooks)
Best for: Production sites with frequent updates
// app/api/revalidate/route.ts
import { createStrapiRevalidator } from 'strapi-nextgen-framework';
export const POST = createStrapiRevalidator({
secret: process.env.REVALIDATION_SECRET!,
tagMap: {
'api::article.article': 'articles',
},
});Pros:
- ✅ Instant updates
- ✅ Efficient (only invalidates changed content)
- ✅ No manual cache clearing
Cons:
- ⚠️ Requires webhook setup
- ⚠️ Depends on Strapi being reachable
2. Time-Based (ISR)
Best for: Sites with predictable update schedules
export const revalidate = 3600; // Hourly updatesPros:
- ✅ Simple setup
- ✅ Predictable regeneration
- ✅ No external dependencies
Cons:
- ⚠️ Updates delayed by revalidation period
- ⚠️ May regenerate unnecessarily
3. Hybrid (Recommended)
Best for: Most production use cases
// Time-based fallback + webhook updates
export const getArticles = unstable_cache(
async () => {
return await strapi.getCollection('articles', GetArticlesDocument);
},
['articles'],
{
revalidate: 3600, // Fallback: hourly
tags: ['articles'], // Webhook: instant
}
);Pros:
- ✅ Instant updates when webhook works
- ✅ Automatic fallback if webhook fails
- ✅ Best of both worlds
Cache Warming Strategies
1. Build-Time Generation
Generate critical pages at build:
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const data = await strapi.getCollection('articles', GetArticlesDocument);
return data.articles?.data.map((article) => ({
slug: article.attributes?.slug,
})) || [];
}Result:
- All blog pages generated at build
- Instant first load
- ISR for new articles
2. Incremental Warming
Gradually build pages on-demand:
// Only generate top 10 at build
export async function generateStaticParams() {
const data = await strapi.getCollection('articles', GetArticlesDocument, {
pagination: { limit: 10 },
sort: 'publishedAt:desc',
});
return data.articles?.data.map((article) => ({
slug: article.attributes?.slug,
})) || [];
}
// Others generated on first visit
export const dynamicParams = true;3. Manual Pre-warming
Trigger cache population:
// scripts/warm-cache.ts
async function warmCache() {
const baseUrl = process.env.SITE_URL;
// Fetch critical pages
await fetch(`${baseUrl}/`);
await fetch(`${baseUrl}/about`);
await fetch(`${baseUrl}/blog`);
console.log('✅ Cache warmed');
}Performance Optimization
1. Minimize Cache Keys
// ❌ Too many cache keys
export const getData = (id: string, lang: string, user: string) =>
unstable_cache(
async () => { /* ... */ },
[`data-${id}-${lang}-${user}`], // Explosion of cache entries
);
// ✅ Minimal cache keys
export const getData = (id: string) =>
unstable_cache(
async () => { /* ... */ },
[`data-${id}`], // One per item
);2. Granular Tags
// ❌ Too broad
revalidateTag('content'); // Invalidates everything
// ✅ Specific
revalidateTag('articles');
revalidateTag(`article-${slug}`);3. Parallel Fetching
// ❌ Sequential
const articles = await strapi.getCollection('articles', GetArticlesDocument);
const pages = await strapi.getCollection('pages', GetPagesDocument);
// ✅ Parallel
const [articles, pages] = await Promise.all([
strapi.getCollection('articles', GetArticlesDocument),
strapi.getCollection('pages', GetPagesDocument),
]);Monitoring Cache Performance
1. Next.js Build Output
npm run build
# Shows:
# ● /blog/[slug] (ISR: 3600s) 1.2kB
# ├ /blog/article-1
# ├ /blog/article-2
# └ /blog/article-32. Cache Hit Rate
// Add logging
export const getArticles = unstable_cache(
async () => {
console.log('🔄 Cache MISS - Fetching from Strapi');
return await strapi.getCollection('articles', GetArticlesDocument);
},
['articles'],
{
revalidate: 3600,
tags: ['articles'],
}
);
// If you see this log often → cache not working
// If you rarely see it → cache working well3. Lighthouse Performance
npm run benchmark:lighthouse
# Check:
# - Time to First Byte (TTFB)
# - First Contentful Paint (FCP)
# - Largest Contentful Paint (LCP)Common Patterns
Pattern 1: List + Detail Pages
// List page: Longer cache
export const getArticlesList = unstable_cache(
async () => strapi.getCollection('articles', GetArticlesDocument),
['articles-list'],
{
revalidate: 3600, // 1 hour OK for list
tags: ['articles'],
}
);
// Detail page: Shorter cache
export const getArticle = (slug: string) => unstable_cache(
async () => strapi.getPage(slug, GetArticleDocument),
[`article-${slug}`],
{
revalidate: 600, // 10 min for fresh content
tags: ['articles', `article-${slug}`],
}
)();Pattern 2: Global + Page Data
// Global data: Rarely changes
export const getGlobal = unstable_cache(
async () => strapi.getGlobal('global', GetGlobalDocument),
['global'],
{
revalidate: 86400, // 24 hours
tags: ['global'],
}
);
// Page data: Changes frequently
export const getPage = (slug: string) => unstable_cache(
async () => strapi.getPage(slug, GetPageDocument),
[`page-${slug}`],
{
revalidate: 600, // 10 minutes
tags: ['pages', `page-${slug}`],
}
)();Pattern 3: User-Specific Data
// ❌ Don't cache user-specific data globally
export const getUserData = (userId: string) => unstable_cache(
async () => fetchUserData(userId),
[`user-${userId}`], // Problem: one cache per user
);
// ✅ Fetch on server, skip cache
export async function getUserData(userId: string) {
return await fetchUserData(userId);
// Next.js automatic request memoization during render
}Troubleshooting
Cache Not Working
Problem: Same data fetched repeatedly
Solutions:
- Check cache tags match in webhook and fetcher
- Verify
revalidateis set - Ensure using
unstable_cacheor route-levelrevalidate - Check production mode (
npm run build && npm start)
Stale Data
Problem: Content updated but site shows old data
Solutions:
- Set up revalidation webhook
- Reduce
revalidatetime - Clear cache:
rm -rf .next - Check webhook logs in Strapi
Too Many Regenerations
Problem: High server load from regenerations
Solutions:
- Increase
revalidatetime - Use more specific cache tags
- Implement rate limiting in webhook
- Consider CDN caching layer
Best Practices
1. Match Cache Duration to Content Type
// News: 5 minutes
export const revalidate = 300;
// Blog: 1 hour
export const revalidate = 3600;
// Legal pages: 1 day
export const revalidate = 86400;2. Always Provide Fallback
// Time-based + tag-based
{
revalidate: 3600, // Fallback
tags: ['articles'], // Preferred
}3. Use Specific Tags
// Multiple specific tags > one broad tag
tags: ['articles', 'featured', `author-${authorId}`]4. Monitor and Adjust
Track cache hit rates and adjust durations based on:
- Content update frequency
- Traffic patterns
- Server capacity