Next.js ISR: Incremental Static Regeneration (2026)
Incremental Static Regeneration (ISR) lets you update statically generated pages after deployment — without rebuilding the entire site. Pages are served from the CDN instantly (like static), but are re-generated in the background on a schedule or on-demand (like SSR). In the App Router era, ISR is expressed through the revalidate option on fetch calls and route segments.
Table of Contents
Next.js Rendering Modes
Next.js App Router determines rendering at the fetch level, not the page level:
- Static — rendered at build time, cached indefinitely (
cache: 'force-cache') - ISR — static, but revalidated on a schedule (
next: { revalidate: 60 }) - Dynamic — rendered per request, never cached (
cache: 'no-store')
no-store, reads dynamic request data (cookies, headers, searchParams) or sets dynamic = 'force-dynamic'.
Time-Based Revalidation
// Revalidate every 60 seconds
async function BlogPage() {
const posts = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 }, // seconds
}).then(r => r.json())
return <PostList posts={posts} />
}
// Segment-level revalidation — applies to all fetches in this route
export const revalidate = 3600 // 1 hour
async function ProductsPage() {
// This fetch inherits the segment revalidate value
const products = await fetch('https://api.example.com/products').then(r => r.json())
return <ProductGrid products={products} />
}
How ISR works in App Router:
- First request: page renders server-side and is cached
- Subsequent requests within the revalidate window: served from cache instantly
- After revalidate window: next request triggers background re-render; stale cache served until regeneration completes
- Regeneration complete: new cache entry served for subsequent requests
On-Demand Revalidation
Trigger revalidation from a webhook or Server Action without waiting for the time window:
// app/api/revalidate/route.ts — webhook endpoint
import { revalidateTag, revalidatePath } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'
export async function POST(request: NextRequest) {
const secret = request.nextUrl.searchParams.get('secret')
if (secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json({ error: 'Invalid secret' }, { status: 401 })
}
const body = await request.json()
// Revalidate by tag — invalidates all fetches tagged 'posts'
revalidateTag('posts')
// Revalidate a specific path
revalidatePath('/blog')
revalidatePath(`/blog/${body.slug}`)
return NextResponse.json({ revalidated: true, now: Date.now() })
}
// Tag fetches for targeted invalidation
async function BlogPage() {
const posts = await fetch('https://api.example.com/posts', {
next: {
revalidate: 3600,
tags: ['posts'], // Tag this fetch
},
}).then(r => r.json())
return <PostList posts={posts} />
}
// Tag with dynamic values
async function PostPage({ params }) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`, {
next: {
revalidate: 86400, // 24 hours
tags: ['posts', `post-${params.slug}`],
},
}).then(r => r.json())
return <Post post={post} />
}
// On-demand: invalidate just one post
revalidateTag(`post-${updatedSlug}`)
// Or all posts
revalidateTag('posts')
Server Action triggered revalidation:
'use server'
import { revalidatePath, revalidateTag } from 'next/cache'
export async function publishPost(postId: string) {
await db.post.update({ where: { id: postId }, data: { published: true } })
revalidateTag('posts')
revalidatePath('/blog')
}
generateStaticParams
Pre-render dynamic route segments at build time:
// app/blog/[slug]/page.tsx
// Tell Next.js which slugs to pre-render
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
return posts.map((post: { slug: string }) => ({
slug: post.slug,
}))
}
// The page component — receives pre-rendered or on-demand params
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`, {
next: { tags: [`post-${params.slug}`] },
}).then(r => r.json())
return <article>{post.content}</article>
}
// Control what happens for slugs NOT in generateStaticParams
export const dynamicParams = true // default — generate on-demand and cache
// export const dynamicParams = false // return 404 for unknown slugs
Segment Cache Config
// Force static — error if dynamic data is used
export const dynamic = 'force-static'
// Force dynamic — never cache this route
export const dynamic = 'force-dynamic'
// Revalidate period for the entire segment
export const revalidate = 3600
// Runtime — 'nodejs' (default) or 'edge'
export const runtime = 'edge'
// Opt specific fetch calls out of caching
const data = await fetch(url, { cache: 'no-store' })
// Cache permanently (override segment revalidate)
const data = await fetch(url, { cache: 'force-cache' })
Partial Prerendering (PPR)
PPR (experimental in Next.js 15) combines static and dynamic rendering at the component level — the static shell renders at build time, and Suspense-wrapped dynamic parts stream in at request time:
// next.config.ts — enable PPR
const nextConfig = {
experimental: {
ppr: true,
},
}
// app/product/[id]/page.tsx — opt in per route
export const experimental_ppr = true
export default function ProductPage({ params }) {
return (
<div>
{/* Static — pre-rendered at build time */}
<ProductInfo id={params.id} />
{/* Dynamic — streams in per request */}
<Suspense fallback={<ReviewsSkeleton />}>
<UserReviews productId={params.id} />
</Suspense>
<Suspense fallback={<RecommendationsSkeleton />}>
<PersonalizedRecommendations />
</Suspense>
</div>
)
}
Common Patterns
Blog with ISR:
// Revalidate list hourly, individual posts daily
// app/blog/page.tsx
export const revalidate = 3600 // list revalidates every hour
// app/blog/[slug]/page.tsx
export async function generateStaticParams() { /* all published slugs */ }
export const revalidate = 86400 // each post revalidates daily
// Webhook from CMS triggers: revalidateTag('posts') on publish
E-commerce with on-demand ISR:
// Products cached for 1 hour
export const revalidate = 3600
// Inventory webhook: revalidatePath(`/products/${sku}`) on stock change
// Price change webhook: revalidateTag('prices')