revalidation-strategy-planner
Revalidation Strategy Planner
Analyze Next.js application routes and recommend optimal caching and revalidation strategies for performance and data freshness.
Overview
To optimize Next.js caching strategies:
- Analyze route characteristics (data freshness requirements, update frequency)
- Determine appropriate rendering strategy (SSG, ISR, SSR, streaming)
- Configure revalidation intervals for ISR routes
- Implement cache tags for on-demand revalidation
- Set up streaming for progressive page loading
Rendering Strategies
Static Site Generation (SSG)
To use SSG for rarely changing content:
// app/about/page.tsx
export default async function AboutPage() {
// Generated at build time, no revalidation
return <div>About Us</div>;
}
Best for:
- Marketing pages
- Documentation
- Static content that rarely changes
Incremental Static Regeneration (ISR)
To use ISR for periodically updated content:
// app/entities/[id]/page.tsx
export const revalidate = 3600; // Revalidate every hour
export default async function EntityPage({ params }: { params: { id: string } }) {
const entity = await fetchEntity(params.id);
return <EntityDetail entity={entity} />;
}
Best for:
- Entity detail pages
- Blog posts
- Product listings
- Content with predictable update patterns
Server-Side Rendering (SSR)
To use SSR for real-time data:
// app/dashboard/page.tsx
export const dynamic = 'force-dynamic';
export default async function Dashboard() {
const data = await fetchUserData();
return <DashboardView data={data} />;
}
Best for:
- User dashboards
- Personalized content
- Real-time data displays
- Authentication-dependent pages
Streaming
To use streaming for progressive loading:
// app/timeline/page.tsx
import { Suspense } from 'react';
export default function TimelinePage() {
return (
<div>
<TimelineHeader />
<Suspense fallback={<TimelineLoader />}>
<TimelineEvents />
</Suspense>
</div>
);
}
Best for:
- Pages with slow data fetching
- Complex pages with multiple data sources
- Improving perceived performance
Consult references/rendering-strategies.md for detailed strategy comparison.
Revalidation Configuration
Time-Based Revalidation
To set revalidation intervals:
// Revalidate every 60 seconds
export const revalidate = 60;
// Revalidate every hour
export const revalidate = 3600;
// Revalidate every day
export const revalidate = 86400;
On-Demand Revalidation
To implement on-demand cache invalidation:
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';
import { NextRequest } from 'next/server';
export async function POST(request: NextRequest) {
const { path, tag } = await request.json();
if (path) {
revalidatePath(path);
}
if (tag) {
revalidateTag(tag);
}
return Response.json({ revalidated: true, now: Date.now() });
}
Use from Server Actions:
'use server';
import { revalidatePath } from 'next/cache';
export async function updateEntity(id: string, data: EntityData) {
await saveEntity(id, data);
revalidatePath(`/entities/${id}`);
revalidatePath('/entities');
}
Cache Tags
To implement cache tag-based revalidation:
// app/entities/[id]/page.tsx
export default async function EntityPage({ params }: { params: { id: string } }) {
const entity = await fetch(`/api/entities/${params.id}`, {
next: {
tags: [`entity-${params.id}`, 'entities'],
},
});
return <EntityDetail entity={entity} />;
}
Revalidate by tag:
import { revalidateTag } from 'next/cache';
// Revalidate all pages with 'entities' tag
revalidateTag('entities');
// Revalidate specific entity
revalidateTag(`entity-${entityId}`);
Reference assets/cache-tag-patterns.ts for cache tagging patterns.
Route Analysis
Use scripts/analyze_routes.py to analyze application routes and recommend strategies:
python scripts/analyze_routes.py ./app
Output includes:
- Route path
- Recommended rendering strategy
- Suggested revalidation interval
- Appropriate cache tags
- Reasoning for recommendations
Analysis Criteria
Consider these factors:
-
Data Freshness Requirements
- Real-time: SSR or very short revalidation (1-60s)
- Near real-time: ISR with short interval (60-300s)
- Periodic updates: ISR with medium interval (300-3600s)
- Rarely changes: SSG or long interval (3600s+)
-
Update Frequency
- Continuous: SSR
- Multiple times per hour: ISR (60-300s)
- Hourly: ISR (3600s)
- Daily: ISR (86400s)
- Weekly+: SSG
-
Personalization
- User-specific: SSR
- Role-based: SSR or ISR with user context
- Public: SSG or ISR
-
Data Source Performance
- Fast (<100ms): Any strategy
- Medium (100-500ms): Consider streaming
- Slow (>500ms): Use streaming or aggressive caching
Consult references/decision-matrix.md for the complete decision matrix.
Implementation Patterns
Entity Detail Pages
To optimize entity pages:
// app/entities/[id]/page.tsx
export const revalidate = 1800; // 30 minutes
export async function generateStaticParams() {
const entities = await fetchAllEntityIds();
return entities.map((id) => ({ id: id.toString() }));
}
export default async function EntityPage({ params }: { params: { id: string } }) {
const entity = await fetchEntity(params.id, {
next: { tags: [`entity-${params.id}`, 'entities'] },
});
return <EntityDetail entity={entity} />;
}
List Pages
To optimize listing pages:
// app/entities/page.tsx
export const revalidate = 300; // 5 minutes
export default async function EntitiesPage({
searchParams,
}: {
searchParams: { page?: string };
}) {
const page = parseInt(searchParams.page || '1');
const entities = await fetchEntities(page, {
next: { tags: ['entities'] },
});
return <EntityList entities={entities} />;
}
Timeline Pages
To optimize timeline with streaming:
// app/timeline/page.tsx
import { Suspense } from 'react';
export default function TimelinePage() {
return (
<div>
<Suspense fallback={<TimelineHeaderSkeleton />}>
<TimelineHeader />
</Suspense>
<Suspense fallback={<EventsSkeleton />}>
<TimelineEvents />
</Suspense>
</div>
);
}
async function TimelineEvents() {
const events = await fetchTimelineEvents({
next: { tags: ['timeline'], revalidate: 600 },
});
return <EventsList events={events} />;
}
Dashboard Pages
To implement personalized dashboard:
// app/dashboard/page.tsx
export const dynamic = 'force-dynamic';
export default async function DashboardPage() {
const session = await getSession();
const data = await fetchUserDashboard(session.userId);
return (
<div>
<Suspense fallback={<StatsSkeleton />}>
<DashboardStats userId={session.userId} />
</Suspense>
<Suspense fallback={<ActivitySkeleton />}>
<RecentActivity userId={session.userId} />
</Suspense>
</div>
);
}
Cache Invalidation Strategies
Granular Invalidation
To invalidate specific resources:
// After entity update
revalidateTag(`entity-${entityId}`);
// After relationship change
revalidateTag(`entity-${sourceId}`);
revalidateTag(`entity-${targetId}`);
revalidateTag('relationships');
Cascade Invalidation
To invalidate related resources:
async function updateEntity(id: string, data: EntityData) {
await saveEntity(id, data);
// Invalidate entity page
revalidateTag(`entity-${id}`);
// Invalidate list pages
revalidateTag('entities');
// Invalidate related pages
const relationships = await getEntityRelationships(id);
for (const rel of relationships) {
revalidateTag(`entity-${rel.targetId}`);
}
}
Batch Invalidation
To invalidate multiple resources efficiently:
async function bulkUpdateEntities(updates: EntityUpdate[]) {
await saveBulkUpdates(updates);
// Collect unique tags
const tags = new Set<string>(['entities']);
for (const update of updates) {
tags.add(`entity-${update.id}`);
}
// Revalidate all at once
for (const tag of tags) {
revalidateTag(tag);
}
}
Performance Optimization
Stale-While-Revalidate
To implement SWR pattern:
export const revalidate = 60; // Revalidate every minute
export const dynamic = 'force-static'; // Serve stale while revalidating
Parallel Data Fetching
To fetch data in parallel:
export default async function EntityPage({ params }: { params: { id: string } }) {
const [entity, relationships, timeline] = await Promise.all([
fetchEntity(params.id),
fetchRelationships(params.id),
fetchTimeline(params.id),
]);
return <EntityDetailView entity={entity} relationships={relationships} timeline={timeline} />;
}
Selective Streaming
To stream only slow components:
export default function EntityPage({ params }: { params: { id: string } }) {
return (
<div>
<EntityHeader id={params.id} /> {/* Fast, no streaming */}
<Suspense fallback={<RelationshipsSkeleton />}>
<EntityRelationships id={params.id} /> {/* Slow, stream it */}
</Suspense>
</div>
);
}
Monitoring and Testing
To monitor cache performance:
- Cache Hit Rates: Track ISR cache hits vs. regenerations
- Revalidation Frequency: Monitor how often pages regenerate
- Response Times: Measure time to first byte (TTFB)
- Stale Serving: Track stale-while-revalidate occurrences
Use Next.js analytics or custom logging:
// middleware.ts
export function middleware(request: NextRequest) {
const start = Date.now();
return NextResponse.next({
headers: {
'x-response-time': `${Date.now() - start}ms`,
},
});
}
Best Practices
- Start Conservative: Begin with shorter revalidation intervals, increase gradually
- Use Cache Tags: Prefer tag-based invalidation over path-based
- Monitor Performance: Track cache hit rates and response times
- Plan Invalidation: Design invalidation strategy with data mutations
- Test Edge Cases: Verify behavior with stale data and revalidation
- Document Decisions: Record why specific intervals were chosen
- Consider Users: Balance freshness with performance
Troubleshooting
Common issues:
- Stale Data Persisting: Check cache tag implementation and invalidation logic
- Excessive Regeneration: Increase revalidation interval or fix trigger-happy invalidation
- Slow Page Loads: Add streaming for slow components
- Cache Not Working: Verify fetch options and dynamic/static configuration
- Development vs Production: Remember ISR only works in production builds