Next.js Static Site Generation in 2026: ISR, generateStaticParams, and On-Demand Revalidation
Master Next.js static generation in 2026: generateStaticParams for dynamic routes, ISR with revalidate, on-demand revalidation via revalidatePath and revalidateTag, and PPR.
Next.js Static Site Generation in 2026: ISR, generateStaticParams, and On-Demand Revalidation
Static generation delivers pre-rendered HTML from CDN edge servers โ sub-100ms TTFB, no database queries on each request, and infinite scalability at low cost. The challenge is keeping that static content fresh. Next.js App Router handles this through three mechanisms: time-based ISR (revalidate), on-demand revalidation (revalidatePath/revalidateTag), and Partial Prerendering (PPR) that mixes static and dynamic in the same page.
This post covers all three patterns: when to use each, generateStaticParams for pre-rendering dynamic routes, cache tagging for granular invalidation, and the ISR stale-while-revalidate behavior that trips up most teams.
The Three Rendering Modes
// Static (SSG): generated once at build time, never refreshes
// Use for: marketing pages, docs, blog posts (manually revalidated)
export const dynamic = "force-static";
// ISR: static + auto-refresh on a schedule
// Use for: product listings, news feeds, dashboards
export const revalidate = 3600; // Regenerate at most once per hour
// Dynamic: server-rendered on every request
// Use for: user dashboards, real-time data, auth-dependent pages
export const dynamic = "force-dynamic";
generateStaticParams: Pre-Rendering Dynamic Routes
// app/blog/[slug]/page.tsx
// This tells Next.js which [slug] values to pre-render at build time
export async function generateStaticParams() {
const posts = await db.blogPost.findMany({
where: { status: "published" },
select: { slug: true },
orderBy: { publishedAt: "desc" },
take: 200, // Pre-render latest 200 posts; others render on-demand
});
return posts.map((post) => ({ slug: post.slug }));
}
// ISR: pages not in generateStaticParams are rendered on first request
// and then cached according to revalidate
export const revalidate = 3600; // Cache for 1 hour
// If a slug not in generateStaticParams is requested:
// - first request: render and cache
// - subsequent requests (within 1h): serve from cache
// - after 1h: serve stale, regenerate in background
export const dynamicParams = true; // Allow on-demand rendering (default)
// Set to false to 404 for slugs not in generateStaticParams
export default async function BlogPostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await db.blogPost.findUnique({
where: { slug, status: "published" },
});
if (!post) notFound();
return <BlogPost post={post} />;
}
// OpenGraph image (also statically generated)
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await db.blogPost.findUnique({
where: { slug },
select: { title: true, excerpt: true, image: true },
});
return {
title: post?.title,
description: post?.excerpt,
openGraph: {
title: post?.title,
description: post?.excerpt,
images: [{ url: post?.image ?? "/og-default.jpg" }],
},
};
}
๐ Looking for a Dev Team That Actually Delivers?
Most agencies sell you a project manager and assign juniors. Viprasol is different โ senior engineers only, direct Slack access, and a 5.0โ Upwork record across 100+ projects.
- React, Next.js, Node.js, TypeScript โ production-grade stack
- Fixed-price contracts โ no surprise invoices
- Full source code ownership from day one
- 90-day post-launch support included
ISR: Stale-While-Revalidate Behavior
// app/products/page.tsx
// ISR with 60-second revalidation
export const revalidate = 60; // Revalidate at most every 60 seconds
export default async function ProductsPage() {
// This fetch is cached by Next.js for 60 seconds
const products = await db.product.findMany({
where: { active: true },
orderBy: { sortOrder: "asc" },
});
return <ProductGrid products={products} />;
}
ISR timeline (important to understand):
T=0s: First request โ render & cache
T=30s: Request โ serve cached (fast, from CDN)
T=60s: Revalidate period expires
T=61s: Request โ serve STALE (still fast) + trigger background regeneration
T=62s: Background regeneration completes โ cache updated
T=63s: Request โ serve fresh content
The key: after revalidation, the next request sees stale content while the background re-render happens. The request after that sees fresh content. This is intentional โ it avoids making users wait for regeneration.
Cache Tags: Granular Invalidation
Tags let you invalidate multiple pages that share the same data:
// app/products/[id]/page.tsx
import { unstable_cache } from "next/cache";
// Tag this fetch with "products" and specific product ID
const getProduct = unstable_cache(
async (id: string) => {
return db.product.findUnique({ where: { id } });
},
["product-detail"], // Cache key prefix
{
tags: ["products", `product-${id}`], // Tags for invalidation
revalidate: 3600,
}
);
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const product = await getProduct(id);
if (!product) notFound();
return <ProductDetail product={product} />;
}
// app/products/page.tsx (listing page โ also tagged)
import { unstable_cache } from "next/cache";
const getProducts = unstable_cache(
async () => db.product.findMany({ where: { active: true } }),
["product-list"],
{ tags: ["products"], revalidate: 3600 }
);
export default async function ProductsPage() {
const products = await getProducts();
return <ProductGrid products={products} />;
}
// app/actions/products.ts โ on-demand revalidation after update
"use server";
import { revalidateTag, revalidatePath } from "next/cache";
export async function updateProduct(productId: string, data: ProductUpdateInput) {
await db.product.update({ where: { id: productId }, data });
// Invalidate all pages tagged with this product OR the products collection
revalidateTag(`product-${productId}`); // โ re-renders /products/[id] page
revalidateTag("products"); // โ re-renders /products listing
// Alternatively, invalidate by path:
// revalidatePath(`/products/${productId}`);
// revalidatePath("/products", "page");
}
๐ Senior Engineers. No Junior Handoffs. Ever.
You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.
- MVPs in 4โ8 weeks, full platforms in 3โ5 months
- Lighthouse 90+ performance scores standard
- Works across US, UK, AU timezones
- Free 30-min architecture review, no commitment
On-Demand Revalidation API Route
For webhooks (e.g., CMS publishes content โ trigger revalidation):
// app/api/revalidate/route.ts
import { NextRequest, NextResponse } from "next/server";
import { revalidatePath, revalidateTag } from "next/cache";
export async function POST(req: NextRequest) {
// Verify the request is from your CMS/webhook source
const secret = req.headers.get("x-revalidate-secret");
if (secret !== process.env.REVALIDATE_SECRET) {
return NextResponse.json({ error: "Invalid secret" }, { status: 401 });
}
const body = await req.json();
const { type, slug, tag } = body;
try {
if (tag) {
// Invalidate by tag (most efficient โ only revalidates tagged pages)
revalidateTag(tag);
return NextResponse.json({ revalidated: true, tag });
}
if (type === "blog-post" && slug) {
revalidatePath(`/blog/${slug}`);
revalidatePath("/blog"); // Invalidate listing too
return NextResponse.json({ revalidated: true, path: `/blog/${slug}` });
}
if (type === "all") {
revalidatePath("/", "layout"); // Revalidate entire site
return NextResponse.json({ revalidated: true, scope: "all" });
}
return NextResponse.json({ error: "Unknown type" }, { status: 400 });
} catch (err) {
return NextResponse.json({ error: "Revalidation failed" }, { status: 500 });
}
}
# Trigger from CMS webhook or CI/CD:
curl -X POST https://yourdomain.com/api/revalidate \
-H "Content-Type: application/json" \
-H "x-revalidate-secret: your-secret" \
-d '{"type": "blog-post", "slug": "nextjs-image-optimization"}'
Partial Prerendering (PPR)
PPR (stable in Next.js 15) renders a static shell at build time and streams dynamic parts at request time โ no loading.tsx flicker:
// next.config.ts
const config: NextConfig = {
experimental: {
ppr: true, // Enable PPR
},
};
// app/products/[id]/page.tsx
import { Suspense } from "react";
// The product shell (title, image, description) is static
// The "In stock" indicator and recommendations are dynamic
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
// This is fetched at build time (static part)
const product = await db.product.findUnique({
where: { id },
select: { id: true, name: true, description: true, image: true, price: true },
});
if (!product) notFound();
return (
<div>
{/* Static shell โ pre-rendered, served instantly */}
<ProductHeader product={product} />
<ProductDescription text={product.description} />
{/* Dynamic parts โ streamed after static shell */}
<Suspense fallback={<StockSkeleton />}>
<StockIndicator productId={id} />
</Suspense>
<Suspense fallback={<RecommendationsSkeleton />}>
<Recommendations productId={id} />
</Suspense>
</div>
);
}
// StockIndicator reads live inventory โ opt into dynamic
async function StockIndicator({ productId }: { productId: string }) {
const stock = await db.inventory.findUnique({ where: { productId } });
return <StockBadge count={stock?.available ?? 0} />;
}
Caching Comparison Table
| Strategy | TTFB | Freshness | Use Case |
|---|---|---|---|
| SSG (build-time only) | <50ms | Stale until redeploy | Docs, landing pages |
| ISR (time-based) | <50ms (stale ok) | Up to X seconds old | Blogs, product listings |
| On-demand revalidation | <50ms | Fresh after webhook | CMS content, editorial |
| PPR | <50ms (shell) | Shell static, parts dynamic | Product pages with live stock |
| Dynamic (SSR) | 100โ500ms | Always fresh | User-specific, real-time |
Cost and Timeline
| Component | Timeline | Cost (USD) |
|---|---|---|
| generateStaticParams setup | 0.5 day | $300โ$500 |
| ISR configuration + cache tag design | 0.5โ1 day | $400โ$800 |
| On-demand revalidation webhook | 0.5 day | $300โ$500 |
| PPR implementation | 1โ2 days | $800โ$1,600 |
| Full SSG/ISR/PPR strategy | 1โ2 weeks | $5,000โ$10,000 |
See Also
- Next.js App Router Caching โ Full cache layer reference
- Next.js Performance Optimization โ LCP/INP/CLS alongside ISR
- Next.js Server Components Patterns โ RSC data fetching with unstable_cache
- AWS CloudFront Edge โ CDN caching + ISR cache headers
Working With Viprasol
We architect Next.js caching strategies for high-traffic applications โ from simple ISR blog pages through complex PPR product pages with live inventory. Our team has shipped Next.js sites serving millions of static pages with sub-100ms TTFB globally.
What we deliver:
- generateStaticParams configuration for all dynamic routes
- ISR revalidate period tuning per content type
- Cache tag taxonomy for granular on-demand invalidation
- Revalidation webhook for CMS integration (Contentful, Sanity, Strapi)
- PPR implementation for pages with mixed static/dynamic content
Explore our web development services or contact us to optimize your Next.js caching strategy.
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Need a Modern Web Application?
From landing pages to complex SaaS platforms โ we build it all with Next.js and React.
Free consultation โข No commitment โข Response within 24 hours
Need a custom web application built?
We build React and Next.js web applications with Lighthouse โฅ90 scores, mobile-first design, and full source code ownership. Senior engineers only โ from architecture through deployment.