ISR

Bài 9 – Cơ chế tái tạo trang tĩnh định kỳ trong Next.js

18/11/2025 DaiPhan
Bài 9 / 18

ISR – Incremental Static Regeneration

ISR (Incremental Static Regeneration) cho phép Next.js tạo ra các trang tĩnh (SSG) nhưng vẫn có khả năng tự làm mới theo chu kỳ mà không cần build lại toàn bộ ứng dụng. Đây là một trong những tính năng mạnh nhất của Next.js, đặc biệt hữu ích cho các trang có dữ liệu thay đổi nhưng không yêu cầu realtime.


1. ISR là gì?

1.1. Định nghĩa và Ưu điểm

ISR kết hợp lợi thế của SSG và SSR:

  • ⚡ Performance: Tốc độ cao như SSG cho lần đầu
  • 🔄 Freshness: Dữ liệu được cập nhật định kỳ
  • 💰 Cost-effective: Không cần server chạy liên tục
  • 🎯 SEO: Vẫn giữ được SEO benefits của static
  • ⚙️ Flexible: Configurable revalidation time

1.2. ISR Flow trong Next.js

First Request → Serve Static → Background Revalidate → Update Cache → Next Request → Serve Fresh

2. ISR trong App Router

2.1. Route-level Revalidation

// app/news/page.tsx – ISR với revalidate 60s
export const revalidate = 60; // Giây

export default async function NewsPage() {
  const res = await fetch("https://api.example.com/news");
  const news = await res.json();

  return (
    <div className="news-container">
      <h1 className="text-3xl font-bold mb-6">📰 Tin tức cập nhật 1 phút/lần</h1>
      <div className="last-updated text-sm text-gray-500 mb-4">
        Last updated: {new Date().toLocaleTimeString()}
      </div>
      <div className="news-grid">
        {news.map((article) => (
          <article key={article.id} className="news-card">
            <h2 className="text-xl font-semibold">{article.title}</h2>
            <p className="text-gray-600 mt-2">{article.summary}</p>
            <div className="news-meta mt-3">
              <span className="text-sm text-gray-500">📅 {article.publishedAt}</span>
              <span className="text-sm text-gray-500 ml-4">👤 {article.author}</span>
            </div>
          </article>
        ))}
      </div>
    </div>
  );
}

2.2. Fetch-level Revalidation

// app/crypto/page.tsx – ISR với fetch revalidate
export default async function CryptoPage() {
  // Có thể set revalidate tại fetch level
  const res = await fetch("https://api.example.com/crypto/prices", {
    next: {
      revalidate: 30 // 30 giây
    }
  });
  const prices = await res.json();

  return (
    <div className="crypto-container">
      <h1 className="text-3xl font-bold mb-6">💰 Giá Crypto (30s update)</h1>
      <div className="crypto-grid">
        {prices.map((crypto) => (
          <div key={crypto.symbol} className="crypto-card">
            <div className="crypto-header">
              <h3 className="font-semibold">{crypto.name}</h3>
              <span className="symbol">{crypto.symbol}</span>
            </div>
            <div className="crypto-price">
              <span className="price">${crypto.price.toFixed(2)}</span>
              <span className={`change ${crypto.change > 0 ? 'positive' : 'negative'}`}>
                {crypto.change > 0 ? '+' : ''}{crypto.change.toFixed(2)}%
              </span>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

2.3. ISR với Dynamic Routes

// app/products/[id]/page.tsx – ISR cho product detail
export const revalidate = 300; // 5 phút

export default async function ProductPage({ params }) {
  const res = await fetch(`https://api.example.com/products/${params.id}`);
  
  if (!res.ok) {
    notFound();
  }
  
  const product = await res.json();

  return (
    <div className="product-detail">
      <div className="product-images">
        <img src={product.image} alt={product.name} />
      </div>
      <div className="product-info">
        <h1 className="text-3xl font-bold">{product.name}</h1>
        <p className="price text-2xl font-semibold text-blue-600">
          ${product.price}
        </p>
        <p className="description mt-4">{product.description}</p>
        <div className="product-stock mt-4">
          <span className={`stock-status ${product.inStock ? 'in-stock' : 'out-of-stock'}`}>
            {product.inStock ? '✅ In Stock' : '❌ Out of Stock'}
          </span>
        </div>
      </div>
    </div>
  );
}

// Generate static params cho tất cả products
export async function generateStaticParams() {
  const res = await fetch('https://api.example.com/products');
  const products = await res.json();
  
  return products.map((product) => ({
    id: product.id.toString(),
  }));
}

3. ISR trong Pages Router (Legacy)

3.1. getStaticProps với Revalidate

// pages/news/index.tsx (Pages Router)
import { GetStaticProps } from 'next'

interface NewsArticle {
  id: string
  title: string
  content: string
  publishedAt: string
}

interface NewsPageProps {
  articles: NewsArticle[]
  lastUpdated: string
}

export default function NewsPage({ articles, lastUpdated }: NewsPageProps) {
  return (
    <div className="news-page">
      <h1 className="text-3xl font-bold mb-6">Latest News</h1>
      <div className="last-updated text-sm text-gray-500 mb-4">
        Last updated: {lastUpdated}
      </div>
      <div className="articles-grid">
        {articles.map((article) => (
          <article key={article.id} className="news-item">
            <h2 className="text-xl font-semibold">{article.title}</h2>
            <p className="text-gray-600 mt-2">{article.content}</p>
            <time className="text-sm text-gray-500 mt-2 block">
              {new Date(article.publishedAt).toLocaleDateString()}
            </time>
          </article>
        ))}
      </div>
    </div>
  );
}

export const getStaticProps: GetStaticProps<NewsPageProps> = async () => {
  const res = await fetch('https://api.example.com/news')
  const articles = await res.json()
  
  return {
    props: {
      articles,
      lastUpdated: new Date().toISOString()
    },
    revalidate: 60 // 60 giây
  }
}

3.2. ISR với Fallback

// pages/blog/[slug].tsx
import { GetStaticProps, GetStaticPaths } from 'next'

interface BlogPost {
  id: string
  title: string
  content: string
  slug: string
}

interface BlogPostProps {
  post: BlogPost
}

export default function BlogPost({ post }: BlogPostProps) {
  return (
    <article className="blog-post">
      <h1 className="text-4xl font-bold mb-4">{post.title}</h1>
      <div className="content">
        <div dangerouslySetInnerHTML={{ __html: post.content }} />
      </div>
    </article>
  );
}

export const getStaticPaths: GetStaticPaths = async () => {
  const res = await fetch('https://api.example.com/blog/posts')
  const posts = await res.json()
  
  // Chỉ pre-render 10 bài đầu tiên
  const paths = posts.slice(0, 10).map((post) => ({
    params: { slug: post.slug }
  }))
  
  return {
    paths,
    fallback: 'blocking' // Các bài còn lại sẽ được ISR khi có request
  }
}

export const getStaticProps: GetStaticProps<BlogPostProps> = async ({ params }) => {
  const res = await fetch(`https://api.example.com/blog/posts/${params.slug}`)
  const post = await res.json()
  
  return {
    props: {
      post
    },
    revalidate: 300 // 5 phút
  }
}

4. Advanced ISR Patterns

4.1. Conditional Revalidation

// app/api/revalidate/route.ts – On-demand revalidation
import { revalidatePath } from 'next/cache'
import { NextRequest } from 'next/server'

export async function POST(request: NextRequest) {
  const { path, secret } = await request.json()
  
  // Validate secret key để đảm bảo security
  if (secret !== process.env.REVALIDATE_SECRET) {
    return Response.json({ message: 'Invalid secret' }, { status: 401 })
  }
  
  try {
    revalidatePath(path)
    return Response.json({ revalidated: true, path })
  } catch (error) {
    return Response.json({ message: 'Error revalidating', error }, { status: 500 })
  }
}

4.2. ISR với Multiple APIs

// app/dashboard/page.tsx – ISR với nhiều data sources
export const revalidate = 120; // 2 phút

export default async function DashboardPage() {
  const [analytics, users, revenue] = await Promise.all([
    fetch('https://api.example.com/analytics', {
      next: { revalidate: 60 }
    }).then(r => r.json()),
    fetch('https://api.example.com/users', {
      next: { revalidate: 120 }
    }).then(r => r.json()),
    fetch('https://api.example.com/revenue', {
      next: { revalidate: 300 }
    }).then(r => r.json())
  ])
  
  return (
    <div className="dashboard">
      <h1 className="text-3xl font-bold mb-6">📊 Dashboard</h1>
      <div className="dashboard-grid">
        <div className="metric-card">
          <h3>Page Views</h3>
          <p className="text-2xl font-bold">{analytics.pageViews}</p>
        </div>
        <div className="metric-card">
          <h3>Total Users</h3>
          <p className="text-2xl font-bold">{users.total}</p>
        </div>
        <div className="metric-card">
          <h3>Revenue</h3>
          <p className="text-2xl font-bold">${revenue.total}</p>
        </div>
      </div>
    </div>
  )
}

5. ISR vs Các Rendering Methods Khác

5.1. Comparison Table

FeatureISRSSGSSRCSR
First Load🚀 Fast🚀 Fastest⚡ Medium🐌 Slow
Data Freshness🔄 Configurable📅 Static🔄 Real-time🔄 Real-time
Server Cost💰 Low💰 Lowest💰💰💰 High💰 Low
SEO✅ Great✅ Best✅ Great❌ Poor
CDN Cacheable✅ Yes✅ Yes❌ No❌ No
Build Time⚡ Fast🐌 Slow🚀 None🚀 None

5.2. Khi nào dùng ISR?

// ✅ NÊN dùng ISR:
// - Blog posts (cập nhật ít thường xuyên)
// - Product catalogs (thay đổi giá)
// - News feeds (cập nhật theo giờ)
// - Analytics dashboards (không cần real-time)
// - Documentation (thay đổi theo release)

// ❌ KHÔNG NÊN dùng ISR:
// - User-specific data
// - Real-time chat
// - Stock trading platforms
// - Live sports scores
// - Authentication-required pages

6. Performance và Best Practices

6.1. Revalidation Strategies

// app/strategies/page.tsx – Different revalidation strategies

// Strategy 1: High-frequency updates (30s - 5m)
export const revalidate = 60; // 1 phút cho news feeds

// Strategy 2: Medium-frequency updates (5m - 1h)
export const revalidate = 900; // 15 phút cho product catalogs

// Strategy 3: Low-frequency updates (1h - 24h)
export const revalidate = 21600; // 6 giờ cho documentation

export default async function StrategiesPage() {
  const data = await fetchData()
  
  return (
    <div>
      <h1>Revalidation Strategies</h1>
      <p>Choose revalidate time based on your content update frequency:</p>
      <ul>
        <li>📰 News: 30s - 5m</li>
        <li>🛍️ Products: 5m - 1h</li>
        <li>📚 Docs: 1h - 24h</li>
      </ul>
    </div>
  )
}

6.2. Error Handling trong ISR

// app/reliable-isr/page.tsx – Robust ISR implementation
export const revalidate = 300 // 5 phút

export default async function ReliableISRPage() {
  try {
    const res = await fetch('https://api.example.com/data', {
      next: { revalidate: 300 }
    })
    
    if (!res.ok) {
      console.error('API Error:', res.status)
      // Fallback to cached data or show error state
      return <ErrorState />
    }
    
    const data = await res.json()
    return <DataDisplay data={data} />
    
  } catch (error) {
    console.error('Fetch Error:', error)
    return <ErrorState />
  }
}

7. Real-world Examples

7.1. E-commerce Product Catalog với ISR

// app/shop/[category]/[product]/page.tsx
export const revalidate = 1800 // 30 phút

export default async function ProductPage({ params }) {
  const [product, relatedProducts, reviews] = await Promise.all([
    fetch(`https://api.example.com/products/${params.product}`, {
      next: { revalidate: 1800 }
    }).then(r => r.json()),
    fetch(`https://api.example.com/products/${params.category}/related`, {
      next: { revalidate: 3600 }
    }).then(r => r.json()),
    fetch(`https://api.example.com/products/${params.product}/reviews`, {
      next: { revalidate: 900 }
    }).then(r => r.json())
  ])
  
  return (
    <div className="product-page">
      <div className="product-main">
        <ProductInfo product={product} />
        <ReviewSection reviews={reviews} />
      </div>
      <aside className="related-products">
        <RelatedProducts products={relatedProducts} />
      </aside>
    </div>
  )
}

7.2. News Website với ISR

// app/news/[category]/page.tsx
export const revalidate = 120 // 2 phút

export default async function NewsCategoryPage({ params }) {
  const [headlines, trending, categoryNews] = await Promise.all([
    fetch(`https://api.example.com/news/${params.category}/headlines`, {
      next: { revalidate: 120 }
    }).then(r => r.json()),
    fetch(`https://api.example.com/news/trending`, {
      next: { revalidate: 300 }
    }).then(r => r.json()),
    fetch(`https://api.example.com/news/${params.category}`, {
      next: { revalidate: 180 }
    }).then(r => r.json())
  ])
  
  return (
    <div className="news-category">
      <section className="headlines">
        <Headlines articles={headlines} />
      </section>
      <aside className="sidebar">
        <Trending articles={trending} />
      </aside>
      <section className="category-news">
        <ArticleGrid articles={categoryNews} />
      </section>
    </div>
  )
}

8. Bài tập thực hành

Bài tập 1: Weather App với ISR

Tạo app thời tiết với:

  • Cập nhật thời tiết mỗi 15 phút
  • Multiple locations
  • Weather icons và animations
  • Responsive design

Bài tập 2: Stock Price Tracker

Tạo stock tracker với:

  • ISR mỗi 5 phút
  • Price change indicators
  • Multiple stock symbols
  • Historical data visualization

Bài tập 3: Blog với On-demand Revalidation

Tạo blog với:

  • ISR mỗi 1 giờ cho posts
  • On-demand revalidation khi có bài mới
  • Categories và tags
  • Search functionality

9. Kết luận bài học

  • ISR: Perfect balance giữa performance và data freshness
  • Use Cases: Content thay đổi định kỳ, không cần real-time
  • Benefits: Fast, cost-effective, SEO-friendly, scalable
  • Trade-offs: Không phù hợp cho real-time data hoặc user-specific content

🔑 GHI NHỚ QUAN TRỌNG:

  • Chọn revalidate time phù hợp với content update frequency
  • Sử dụng on-demand revalidation cho critical updates
  • Monitor performance và adjust revalidate times khi cần
  • Combine ISR với client-side fetching cho real-time features
  • Test different revalidate strategies để tối ưu performance
18 bài học
Bài 9
Tiến độ hoàn thành 50%

Đã hoàn thành 9/18 bài học