VICTOR
DAJ
Next.js Expert
Headless CMS
Content Architecture

Let's talk

Tell me about your project

Back to Blog
Next.js

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.

December 20, 2025 · 7 min read
Mastering ISR: Next.js Incremental Static Regeneration

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.

Share on

Victor Daj

Victor Daj

December 20, 2025

Free Course

Learn Headless CMS

Get instant access • 30 min • No fluff

No spam. Unsubscribe anytime.