Next.js
7 minutes readChoosing the Right Rendering Strategy in Next.js
A practical decision framework for static rendering, dynamic server rendering, revalidation, and client-side interactivity in the App Router.

Rendering is a per-route and often per-data-source decision. Static output is excellent for public content, request-time rendering is needed for personalized responses, ISR balances freshness and reuse, and client-side fetching fits browser-owned interactions. The examples below are patterns to adapt, not settings to copy blindly; verify the behavior against the Next.js version you deploy.
1. Static Site Generation (SSG)
Use static generation for public pages whose content can be prepared ahead of a request: an about page, documentation, a portfolio, or published articles. It reduces per-request server work and can be served from a CDN. In the App Router, pages that only use static data and do not opt into request-time APIs are often statically rendered by default; force-static is an explicit option, not a universal performance switch.
// Public content that changes only when you publish a new build
import { posts } from "@/blog/data/posts";
export const dynamic = "force-static";
export default function BlogPage() {
return <BlogList posts={posts} />;
}2. Incremental Static Regeneration (ISR)
ISR is useful when public data changes but does not need a new origin render for every visitor: catalog pages, public product details, or news lists. revalidate = 300 expresses a freshness window of five minutes, subject to the framework’s revalidation semantics and deployment platform. For publish events, on-demand invalidation with a tag or path can be more precise than waiting for the time window.
// Reuse the generated response and refresh it at most every 5 minutes
export const revalidate = 300;
export default async function ProductsPage() {
const products = await getPublicProducts();
return <ProductGrid products={products} />;
}3. Dynamic server rendering (SSR)
Use request-time rendering when output depends on a session, cookies, headers, or other request-specific information. The example reads a session from cookies and loads the account for that user. Do not cache personalized HTML or data in a shared cache. Also remember that forcing dynamic rendering is not a replacement for authorization checks inside every sensitive data operation.
import { cookies } from "next/headers";
export const dynamic = "force-dynamic";
export default async function AccountPage() {
const cookieStore = await cookies();
const session = await readSession(cookieStore);
if (!session) redirect("/login");
const account = await getAccount(session.userId);
return <AccountDashboard account={account} />;
}4. Client-side rendering and fetching (CSR)
Client-side fetching is useful for data that changes frequently or is tied to an interaction after the page loads, such as a live quote or a user-controlled filter. Keep the client boundary small, provide loading and error states, cancel obsolete requests, and avoid exposing private server credentials. For production applications, a data-fetching library can add caching, deduplication, retries, and focus revalidation more safely than repeating ad-hoc effects.
"use client";
import { useEffect, useState } from "react";
type Quote = { symbol: string; price: number };
export function LiveStock({ symbol }: { symbol: string }) {
const [quote, setQuote] = useState<Quote | null>(null);
const [error, setError] = useState(false);
useEffect(() => {
const controller = new AbortController();
fetch(`/api/quotes/${symbol}`, { signal: controller.signal })
.then((r) => { if (!r.ok) throw new Error(); return r.json(); })
.then(setQuote)
.catch((e) => { if (e.name !== "AbortError") setError(true); });
return () => controller.abort();
}, [symbol]);
if (error) return <p role="alert">Quote unavailable.</p>;
if (!quote) return <p aria-live="polite">Loading quote…</p>;
return <p>{quote.symbol}: {quote.price}</p>;
}5. Pre-render dynamic routes with generateStaticParams
For known public slugs, generateStaticParams lets the build prepare route output ahead of time. dynamicParams = false means unknown slugs return 404 instead of being generated on demand. Do not use this for an unbounded or rapidly changing set without considering build time and revalidation.
import { posts } from "@/blog/data/posts";
export function generateStaticParams() {
return posts.map((post) => ({ slug: post.slug }));
}
export const dynamicParams = false; // 404 for slugs not generated above6. Revalidate after a mutation
When an editor publishes a product, waiting for a time-based window may leave stale output visible. After a successful database write, invalidate the specific tag and/or route. Match the tag used by the data fetch and verify the API signature for your Next.js version, since cache APIs have changed between releases.
"use server";
import { revalidatePath, revalidateTag } from "next/cache";
export async function publishProduct(id: string) {
await updateProductInDatabase(id);
revalidateTag("products");
revalidatePath("/products");
}7. Choose by page type
- Portfolio, docs, published blog post → SSG; use revalidation if content updates without a rebuild.
- Public catalog with periodic updates → ISR or tagged/path revalidation.
- Account, billing, admin data → request-time rendering plus server-side authorization; do not shared-cache private output.
- Live filters, charts, rapidly changing client-only widgets → small Client Component, often backed by a query library.
8. Verify in a production build
Use next build and inspect the route output summary instead of assuming a page is static because its code looks simple. Test with realistic data, authentication, slow upstream services, and a deployed environment. Rendering is a trade-off between freshness, latency, server cost, cache safety, and user experience—not a contest to make every route static.