Modern teams love Static Site Generation (SSG) for its speed and reliability. But as your site grows—and content changes frequently—pure SSG starts to hurt: builds get slow, pages go stale, and redeploys become bottlenecks. Incremental Static Generation (ISR) is the practical pattern that keeps static fast while making it fresh.
1) The problem with pure SSG
SSG pre-renders pages at build time and serves them from a CDN. That’s fantastic for performance but introduces a few pain points at scale:
- Stale content: Once deployed, pages don’t update until the next build. News, pricing, stock, and listings drift out of date.
- Long build times: Large sites require generating thousands of pages in a single build, slowing CI/CD.
- All-or-nothing deployments: A tiny copy change requires a whole-site rebuild & redeploy.
- Operational friction: Content teams depend on developer-triggered deploys to publish updates.
Goal: Keep static speed, reduce staleness, and decouple content updates from full redeploys.
2) How ISR solves it (mental model)
Incremental Static Regeneration (ISR) allows pages to be statically generated and regenerated after a configurable interval or on demand—without a full rebuild.
- Serve static instantly from cache/CDN.
- Revalidate the page in the background when it’s “too old” or when a change is pushed.
- Next request gets the fresh page; failures keep the last good version.
- Granular control: choose per-page timing or trigger updates per path or per data “tag”.
This follows a familiar pattern: cache + time/notification-based invalidation, analogous to stale‑while‑revalidate.
3) How ISR works under the hood (flow)
- First request after build renders and caches the static HTML.
- Subsequent requests are served from cache (fast).
- When the page exceeds its revalidate window (e.g., 60s), a request triggers background regeneration.
- If regeneration succeeds, the cache swaps in the fresh HTML atomically.
- If regeneration fails, the old version remains (graceful degradation).
You control when this happens (time-based) and what gets refreshed (path- or tag-based on-demand revalidation).
4) Code: Pages Router (Next.js 12/13+)
a) Time-based revalidation with getStaticProps
// pages/products/[id].tsx
import { GetStaticPaths, GetStaticProps } from 'next';
export const getStaticPaths: GetStaticPaths = async () => {
// Pre-render a subset; fall back for the rest
const ids = await fetchFeaturedIds();
return {
paths: ids.map((id) => ({ params: { id } })),
fallback: 'blocking', // or true (shows a fallback UI)
};
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
const product = await fetchProduct(params!.id as string);
if (!product) {
return { notFound: true };
}
return {
props: { product },
// Re-generate this page in the background at most once per 60 seconds
revalidate: 60,
};
};
export default function ProductPage({ product }: { product: Product }) {
return <ProductView product={product} />;
}
Notes
-
fallback: 'blocking'avoids a visible loading state for uncached paths. -
revalidate: 60means the page may be regenerated in the background at most once per 60 seconds after it becomes stale.
b) On-demand revalidation (Pages Router)
Create an API route that your CMS/webhook can call after content changes:
// pages/api/revalidate.ts
import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
// 1) Verify a secret to prevent abuse
const secret = process.env.REVALIDATE_SECRET;
if (req.query.secret !== secret) {
return res.status(401).json({ message: 'Invalid token' });
}
try {
const { path } = req.query;
if (typeof path !== 'string') {
return res.status(400).json({ message: 'Missing ?path=' });
}
// 2) Revalidate the given path
await res.revalidate(path); // e.g., "/products/123"
return res.json({ revalidated: true, path });
} catch (err) {
return res.status(500).json({ revalidated: false, error: (err as Error).message });
}
}
How to use
- Configure your CMS to hit
/api/revalidate?secret=...&path=/products/123after content updates. - Only the changed page refreshes—no redeploy required.
5) Code: App Router (Next.js 13/14+)
With the App Router, you can control revalidation per route or per fetch, and use tag‑based invalidation for powerful CMS workflows.
a) Time-based revalidation per route
// app/blog/[slug]/page.tsx
export const revalidate = 120; // seconds
import { getPostBySlug } from '@/lib/posts';
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug);
if (!post) return notFound();
return <Article post={post} />;
}
b) Time-based revalidation per fetch
// app/lib/data.ts
export async function getProducts() {
const res = await fetch('https://api.example.com/products', {
// Revalidate this fetch every 60s
next: { revalidate: 60 },
});
return res.json();
}
c) Tag-based caching & revalidation (recommended for CMS-driven content)
Tag your fetches, then revalidate by tag when content changes:
// app/lib/data.ts
export async function getPost(slug: string) {
const res = await fetch(`https://cms.example.com/posts/${slug}`, {
next: { tags: ['posts', `post:${slug}`] },
});
return res.json();
}
Trigger revalidation by tag in a Route Handler:
// app/api/revalidate/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { revalidatePath, revalidateTag } from 'next/cache';
export async function POST(req: NextRequest) {
const { secret, path, tag } = await req.json();
if (secret !== process.env.REVALIDATE_SECRET) {
return NextResponse.json({ message: 'Invalid token' }, { status: 401 });
}
if (tag) {
await revalidateTag(tag); // e.g., "posts" or `post:my-slug`
}
if (path) {
await revalidatePath(path); // e.g., "/blog/my-slug"
}
return NextResponse.json({ revalidated: true, path, tag });
}
Why tags?
- Many pages share the same data (e.g., a “latest posts” list). Revalidating by tag refreshes every page that used that data—without hunting down each path.
6) Updating existing pages in practice
You have three primary strategies:
-
Time-based (hands-off): choose a sensible
revalidateinterval per page or per fetch. -
Event-driven (precise): call
/api/revalidate(Pages) or/api/revalidate(App Route) when CMS content changes. - Hybrid (safe default): short time-based windows + event-driven hooks for critical updates.
Example: CMS webhook → App Router
- CMS publishes a post and issues
POST /api/revalidatewith{ secret, tag: "posts" }. - All routes fetching with
{ next: { tags: ['posts'] } }refresh automatically on next request. - Editors see updates immediately; visitors get static speed.
7) Pros of ISR
- Static-speed performance: Low TTFB from CDN/caches, great Core Web Vitals.
- Freshness without redeploys: Background regeneration and on-demand invalidation keep content up to date.
- Build-time wins: No need to pre-render every page; generate on first request and refresh incrementally.
- Cost & scale: Less server work than full SSR on every request; avoids monolithic rebuilds.
- Resilience: If regeneration fails, the last good page continues to serve.
- SEO-friendly: Crawlable, stable HTML with timely updates.
- Great with CMS: Tag-based revalidation maps cleanly to content models and editorial workflows.
8) Caveats & good practices
- Highly personalized pages still need SSR or client-side rendering.
- Very volatile data (e.g., live prices) may require shorter revalidate windows or SSR.
- Atomic multi-page updates: prefer tag-based revalidation so related pages refresh together.
-
Fallback UX: if you use
fallback: true, ensure a good loading skeleton for first render. - Error handling: log regeneration failures; consider alerting for critical pages.
- Cache-awareness: understand your platform’s CDN and region behavior to avoid surprise staleness.
9) Conclusion
ISR gives you the best of both worlds: the speed and stability of static sites with the freshness of dynamic systems. Adopt a cache-first mindset: render statically, revalidate by time, and revalidate by event/tag when content changes. Start with conservative intervals (e.g., 60–120s), wire up CMS webhooks, and reach for SSR only when personalization or ultra-fresh data truly demands it.
TL;DR: Ship fast static pages. Keep them fresh—incrementally.
Quick Reference
- Pages Router:
getStaticProps+revalidate,res.revalidate(path) - App Router:
export const revalidate,fetch(..., { next: { revalidate, tags } }),revalidatePath,revalidateTag - Good defaults:
revalidate: 60–300for listings; on-demand for critical content
Top comments (0)