Incremental Static Regeneration (ISR): Best of Both Worlds
Incremental Static Regeneration (ISR) is a hybrid rendering approach that combines the performance benefits of Static Site Generation with the ability to update content without rebuilding the entire site. It's perfect for content that changes occasionally but doesn't need to be real-time.
How ISR Works
ISR Flow in Next.js 15
When to Use ISR
Perfect For:
- E-commerce Product Pages: Products that update occasionally
- Blog Posts: Articles that might be edited after publishing
- News Articles: Content that gets minor updates
- Documentation: Pages that are updated periodically
- Marketing Pages: Landing pages with occasional content changes
- API-heavy Content: Data that changes but doesn't need to be real-time
Not Ideal For:
- Real-time Data: Stock prices, live feeds, chat applications
- User-specific Content: Personalized dashboards
- Frequently Changing Data: Social media feeds, live sports scores
Implementation with Next.js 15 App Router
Basic ISR Page
// app/products/[id]/page.tsx
import { Metadata } from 'next'
import { notFound } from 'next/navigation'
interface Product {
id: string
name: string
description: string
price: number
stock: number
lastUpdated: string
category: string
images: string[]
}
interface PageProps {
params: { id: string }
}
// Generate static params for popular products
export async function generateStaticParams() {
const popularProducts = await fetch('https://api.example.com/products/popular').then(res => res.json())
return popularProducts.map((product: { id: string }) => ({
id: product.id,
}))
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const product = await fetch(`https://api.example.com/products/${params.id}`).then(res => res.json())
return {
title: `${product.name} - Our Store`,
description: product.description,
openGraph: {
images: product.images,
type: 'product',
},
}
}
// This page will be statically generated and revalidated every 3600 seconds (1 hour)
export default async function ProductPage({ params }: PageProps) {
const product: Product = await fetch(`https://api.example.com/products/${params.id}`, {
next: { revalidate: 3600 }, // Revalidate every hour
}).then(res => res.json())
if (!product) {
notFound()
}
return (
<div className="max-w-6xl mx-auto py-8 px-4">
<div className="grid md:grid-cols-2 gap-8">
<div>
<img
src={product.images[0]}
alt={product.name}
className="w-full rounded-lg"
/>
</div>
<div>
<h1 className="text-3xl font-bold mb-4">{product.name}</h1>
<p className="text-2xl font-semibold text-green-600 mb-4">
${product.price.toFixed(2)}
</p>
<p className="text-gray-600 mb-6">{product.description}</p>
<div className="mb-6">
<span className={`font-semibold ${product.stock > 0 ? 'text-green-600' : 'text-red-600'}`}>
{product.stock > 0 ? `In Stock (${product.stock})` : 'Out of Stock'}
</span>
</div>
<button
className="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50"
disabled={product.stock === 0}
>
{product.stock > 0 ? 'Add to Cart' : 'Out of Stock'}
</button>
<p className="text-sm text-gray-500 mt-4">
Last updated: {new Date(product.lastUpdated).toLocaleString()}
</p>
</div>
</div>
</div>
)
}
Blog with ISR
// app/blog/[slug]/page.tsx
import { Metadata } from 'next'
import { notFound } from 'next/navigation'
interface BlogPost {
slug: string
title: string
content: string
publishedAt: string
updatedAt: string
author: {
name: string
avatar: string
}
tags: string[]
readTime: number
}
interface PageProps {
params: { slug: string }
}
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(res => res.json())
return posts.map((post: { slug: string }) => ({
slug: post.slug,
}))
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(res => res.json())
return {
title: post.title,
description: post.content.substring(0, 160),
openGraph: {
title: post.title,
description: post.content.substring(0, 160),
type: 'article',
publishedTime: post.publishedAt,
modifiedTime: post.updatedAt,
authors: [post.author.name],
},
}
}
export default async function BlogPostPage({ params }: PageProps) {
const post: BlogPost = await fetch(`https://api.example.com/posts/${params.slug}`, {
next: { revalidate: 1800 }, // Revalidate every 30 minutes
}).then(res => res.json())
if (!post) {
notFound()
}
return (
<article className="max-w-4xl mx-auto py-8 px-4">
<header className="mb-8">
<h1 className="text-4xl font-bold mb-4">{post.title}</h1>
<div className="flex items-center space-x-4 text-gray-600 mb-4">
<div className="flex items-center space-x-2">
<img
src={post.author.avatar}
alt={post.author.name}
className="w-8 h-8 rounded-full"
/>
<span>{post.author.name}</span>
</div>
<time>{new Date(post.publishedAt).toLocaleDateString()}</time>
<span>{post.readTime} min read</span>
{post.updatedAt !== post.publishedAt && (
<span className="text-sm">
(Updated: {new Date(post.updatedAt).toLocaleDateString()})
</span>
)}
</div>
<div className="flex space-x-2">
{post.tags.map((tag) => (
<span
key={tag}
className="px-3 py-1 bg-gray-100 text-gray-700 rounded-full text-sm"
>
{tag}
</span>
))}
</div>
</header>
<div className="prose lg:prose-xl max-w-none">
{post.content}
</div>
</article>
)
}
News Article with ISR
// app/news/[id]/page.tsx
import { Metadata } from 'next'
import { notFound } from 'next/navigation'
interface NewsArticle {
id: string
title: string
content: string
publishedAt: string
updatedAt: string
category: string
author: string
imageUrl: string
readTime: number
}
interface PageProps {
params: { id: string }
}
export async function generateStaticParams() {
const articles = await fetch('https://api.example.com/news/featured').then(res => res.json())
return articles.map((article: { id: string }) => ({
id: article.id,
}))
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const article = await fetch(`https://api.example.com/news/${params.id}`).then(res => res.json())
return {
title: article.title,
description: article.content.substring(0, 160),
openGraph: {
title: article.title,
description: article.content.substring(0, 160),
images: [article.imageUrl],
type: 'article',
publishedTime: article.publishedAt,
modifiedTime: article.updatedAt,
},
}
}
export default async function NewsArticlePage({ params }: PageProps) {
const article: NewsArticle = await fetch(`https://api.example.com/news/${params.id}`, {
next: { revalidate: 900 }, // Revalidate every 15 minutes
}).then(res => res.json())
if (!article) {
notFound()
}
return (
<article className="max-w-4xl mx-auto py-8 px-4">
<header className="mb-8">
<div className="mb-4">
<span className="px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm font-medium">
{article.category}
</span>
</div>
<h1 className="text-4xl font-bold mb-4">{article.title}</h1>
<div className="flex items-center space-x-4 text-gray-600 mb-6">
<span>By {article.author}</span>
<time>{new Date(article.publishedAt).toLocaleDateString()}</time>
<span>{article.readTime} min read</span>
{article.updatedAt !== article.publishedAt && (
<span className="text-sm">
Updated: {new Date(article.updatedAt).toLocaleDateString()}
</span>
)}
</div>
<img
src={article.imageUrl}
alt={article.title}
className="w-full h-64 object-cover rounded-lg"
/>
</header>
<div className="prose lg:prose-xl max-w-none">
{article.content}
</div>
</article>
)
}
Advanced ISR Patterns
Conditional Revalidation
// app/products/[id]/page.tsx
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await fetch(`https://api.example.com/products/${params.id}`).then(res => res.json())
// Different revalidation times based on product type
const revalidationTime = product.category === 'electronics' ? 1800 : 3600 // 30 min vs 1 hour
const productWithRevalidation = await fetch(`https://api.example.com/products/${params.id}`, {
next: { revalidate: revalidationTime },
}).then(res => res.json())
return <div>{/* Product content */}</div>
}
On-Demand Revalidation
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'
export async function POST(request: NextRequest) {
try {
const { path, token } = await request.json()
// Verify the token (implement your own verification logic)
if (token !== process.env.REVALIDATION_TOKEN) {
return NextResponse.json({ message: 'Invalid token' }, { status: 401 })
}
// Revalidate specific path
revalidatePath(path)
return NextResponse.json({ message: 'Revalidated successfully' })
} catch (error) {
return NextResponse.json({ message: 'Error revalidating' }, { status: 500 })
}
}
// Usage: Trigger revalidation when content changes
// POST /api/revalidate
// Body: { "path": "/products/123", "token": "your-secret-token" }
Tag-based Revalidation
// app/products/[id]/page.tsx
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await fetch(`https://api.example.com/products/${params.id}`, {
next: {
revalidate: 3600,
tags: [`product-${params.id}`, 'products']
},
}).then(res => res.json())
return <div>{/* Product content */}</div>
}
// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache'
export async function POST(request: NextRequest) {
const { tag } = await request.json()
// Revalidate all products with this tag
revalidateTag(tag)
return NextResponse.json({ message: 'Revalidated successfully' })
}
Performance Optimization
Selective ISR
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(res => res.json())
// Only pre-generate popular posts
const popularPosts = posts.filter((post: any) => post.views > 1000)
return popularPosts.map((post: { slug: string }) => ({
slug: post.slug,
}))
}
export default async function BlogPostPage({ params }: { params: { slug: string } }) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`, {
next: { revalidate: 3600 },
}).then(res => res.json())
return <div>{/* Post content */}</div>
}
Fallback Strategies
// app/products/[id]/page.tsx
export async function generateStaticParams() {
const products = await fetch('https://api.example.com/products').then(res => res.json())
return products.map((product: { id: string }) => ({
id: product.id,
}))
}
// Handle fallback for non-pre-generated pages
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await fetch(`https://api.example.com/products/${params.id}`, {
next: { revalidate: 3600 },
}).then(res => res.json())
if (!product) {
notFound()
}
return <div>{/* Product content */}</div>
}
Monitoring and Analytics
// Track ISR performance
export default async function Page({ params }: { params: { id: string } }) {
const startTime = Date.now()
const data = await fetch(`https://api.example.com/data/${params.id}`, {
next: { revalidate: 3600 },
}).then(res => res.json())
// Log performance metrics
console.log(`ISR fetch took ${Date.now() - startTime}ms for ${params.id}`)
return <div>{/* Your content */}</div>
}
Best Practices
1. Choose Appropriate Revalidation Times
// Different revalidation strategies
const revalidationTimes = {
'breaking-news': 300, // 5 minutes
'regular-news': 1800, // 30 minutes
'blog-posts': 3600, // 1 hour
'product-pages': 7200, // 2 hours
'static-content': 86400, // 24 hours
}
2. Handle Errors Gracefully
export default async function Page({ params }: { params: { id: string } }) {
try {
const data = await fetch(`https://api.example.com/data/${params.id}`, {
next: { revalidate: 3600 },
}).then(res => {
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`)
return res.json()
})
return <div>{/* Your content */}</div>
} catch (error) {
// Return cached version or fallback
return <div>Content temporarily unavailable</div>
}
}
3. Optimize Data Fetching
// Parallel data fetching with ISR
export default async function Dashboard() {
const [user, orders, analytics] = await Promise.all([
fetch('https://api.example.com/user', { next: { revalidate: 3600 } }).then(res => res.json()),
fetch('https://api.example.com/orders', { next: { revalidate: 1800 } }).then(res => res.json()),
fetch('https://api.example.com/analytics', { next: { revalidate: 7200 } }).then(res => res.json()),
])
return <div>{/* Your content */}</div>
}
ISR is perfect for content that needs to be fresh but doesn't require real-time updates. It provides excellent performance while ensuring content stays current through background revalidation.