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.