Mastering ISR: Next.js Incremental Static Regeneration
How to leverage ISR for the perfect balance between static performance and dynamic content freshness in your headless CMS setup.

Mastering ISR: Next.js Incremental Static Regeneration
Static sites are fast but stale. Server-rendered sites are fresh but slow. ISR gives you both—and it's a game-changer for headless CMS architectures.
The Problem with Traditional Approaches
Static Generation (SSG)
- ✅ Blazing fast (served from CDN)
- ❌ Requires full rebuild for content changes
- ❌ Build times grow with content volume
Server-Side Rendering (SSR)
- ✅ Always fresh content
- ❌ Every request hits your server
- ❌ Slower Time to First Byte (TTFB)
Enter ISR: The Best of Both Worlds
ISR lets you update static content after deployment without rebuilding your entire site.
// app/blog/[slug]/page.tsx
export const revalidate = 60 // Revalidate every 60 seconds
export default async function BlogPost({ params }) {
const post = await getPost(params.slug)
return <Article post={post} />
}
On-Demand Revalidation
For CMS webhooks, you can trigger revalidation immediately when content changes:
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from "next/cache";
import { NextRequest } from "next/server";
export async function POST(request: NextRequest) {
const { slug, secret } = await request.json();
// Verify webhook secret
if (secret !== process.env.REVALIDATION_SECRET) {
return Response.json({ error: "Invalid secret" }, { status: 401 });
}
// Revalidate the specific post
revalidatePath(`/blog/${slug}`);
// Or revalidate by tag
revalidateTag("posts");
return Response.json({ revalidated: true });
}
Setting Up CMS Webhooks
Sanity
// sanity.config.ts
export default defineConfig({
// ...
document: {
actions: (prev) =>
prev.map((action) => {
if (action.action === "publish") {
return (props) => {
const originalAction = action(props);
return {
...originalAction,
onHandle: async () => {
await originalAction.onHandle?.();
// Trigger revalidation
await fetch("/api/revalidate", {
method: "POST",
body: JSON.stringify({
slug: props.draft?.slug?.current,
secret: process.env.REVALIDATION_SECRET,
}),
});
},
};
};
}
return action;
}),
},
});
Cache Tags for Granular Control
Next.js 14+ supports cache tags for fine-grained revalidation:
// lib/sanity.ts
import { unstable_cache } from "next/cache";
export const getPosts = unstable_cache(
async () => {
return client.fetch('*[_type == "post"]');
},
["posts"],
{ tags: ["posts"], revalidate: 3600 },
);
Performance Comparison
| Strategy | TTFB | Freshness | Build Time |
|---|---|---|---|
| SSG | ~50ms | Stale until rebuild | Long |
| SSR | ~200ms | Always fresh | N/A |
| ISR (60s) | ~50ms | Max 60s stale | Short |
| ISR + On-demand | ~50ms | Near-instant | Short |
ISR with on-demand revalidation is my go-to strategy for headless CMS projects. You get CDN-level performance with CMS-level freshness.










