Rendering Strategies
13 of 13
Static vs Dynamic Renderingbasicrendering
Static: rendered at build time, same HTML for everyone. Dynamic: rendered per request for personalized or fresh content.
Builds onFile-based Routing & Special Files
Key Points
- Static (default): route rendered at build time when no runtime APIs are accessed. Served instantly from CDN.
- Dynamic: automatically opted in when accessing cookies(), headers(), searchParams, or uncached data.
- In Next.js 15, fetch() is NOT cached by default , but a route can still be static if no runtime APIs are used.
- export const dynamic = "force-dynamic" , always renders dynamically, every request hits the server.
- export const dynamic = "force-static" , forces static even if runtime APIs are accessed (they return empty).
- generateStaticParams: pre-generate known dynamic paths statically; unknown paths fall back to dynamic.
// Force static
export const dynamic = 'force-static';
// Force dynamic
export const dynamic = 'force-dynamic';
// These automatically opt into dynamic rendering:
const cookieStore = await cookies(); // ← makes route dynamic
const headersList = await headers(); // ← makes route dynamic
const { q } = await searchParams; // ← makes route dynamicWarning
Gotcha
Calling cookies() or headers() anywhere in a Server Component automatically opts the entire page into dynamic rendering , even if the result isn't used. Extract them as close to the consuming component as possible.
Static Site Generation (SSG)basicrendering
Pages rendered at build time into static HTML , served instantly from a CDN with zero server cost per request.
Builds onStatic vs Dynamic Rendering
Key Points
- Add "use cache" to a function or component , its output is baked into the static shell at build time.
- generateStaticParams() declares which dynamic paths (e.g., blog slugs) are pre-generated at build time.
- Paths NOT in generateStaticParams render dynamically on first request, then cached.
- cacheLife("days") or cacheLife("weeks") controls how long the cached output is valid.
- Benefits: instant load times, global CDN distribution, no server involved per request.
async function BlogPosts() {
'use cache';
cacheLife('days'); // regenerate after 24 hours
const posts = await fetchPosts();
return <PostList posts={posts} />;
}
// Pre-generate /blog/[slug] at build time
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((p) => ({ slug: p.slug }));
}Server-Side Rendering (SSR)basicrendering
Pages rendered on the server per request , always fresh data, personalized per user.
Builds onStatic vs Dynamic Rendering
Key Points
- A route is dynamically rendered when it uses: cookies(), headers(), or searchParams.
- fetch() without "use cache" runs fresh on every request , implicitly dynamic.
- Dynamic rendering guarantees up-to-date data but adds server latency per request.
- Even fully dynamic pages benefit from streaming , the static shell (layout) is sent to the browser immediately.
- Combine static and dynamic on the same page using Suspense: static shell instant, dynamic parts stream in.
// Dynamically rendered , per-request, personalized
export default async function ProfilePage() {
// cookies() opts the whole page into dynamic rendering
const userId = (await cookies()).get('userId')?.value;
const profile = await fetchProfile(userId); // fresh every request
return <Profile data={profile} />;
}Incremental Static Regeneration (ISR)intermediaterendering
Revalidate static pages after a time interval or on demand , stale-while-revalidate without a full rebuild.
Builds onStatic Site Generation (SSG)
Key Points
- Time-based: cacheLife("hours") , cached for that duration, then revalidated in the background.
- On-demand: cacheTag("posts") marks cached content; updateTag("posts") in a Server Action instantly expires it.
- Stale-while-revalidate: serve the cached page immediately while revalidating in the background.
- Legacy API: export const revalidate = 60 (seconds) or fetch(url, { next: { revalidate: 60 } }) still work.
- Per-fetch ISR: cache individual fetches independently from the page-level cache.
Note
Textbook Definition
ISR lets you update statically generated pages after the initial build. Next.js 15 uses "use cache" with cacheLife() for time-based ISR and cacheTag() + updateTag() for on-demand invalidation.
// Time-based ISR
async function Products() {
'use cache';
cacheLife('hours'); // revalidate every hour
const products = await db.products.findAll();
return <ProductGrid products={products} />;
}
// On-demand ISR via Server Action
async function publishProduct(data) {
'use server';
await db.products.create(data);
updateTag('products'); // immediately bust the cache
}Warning
Gotcha
On-demand revalidation (updateTag) only invalidates the cache on the server instance that handled the request. In multi-instance/serverless deployments, use a shared cache store ('use cache: remote') to propagate invalidation everywhere.
loading.tsx , Route-Level Streamingbasicstreaming
Place loading.tsx beside page.tsx to show instant loading UI for the entire route , no manual Suspense boilerplate needed.
Builds onStatic vs Dynamic Rendering
Key Points
- Add loading.tsx in any route folder , Next.js automatically wraps page.tsx in <Suspense fallback={<Loading />}>.
- The layout renders and is sent immediately; loading.tsx shows while the page awaits data.
- User sees layout + loading state instantly on navigation , content swaps in when ready.
- loading.tsx is a Server Component , render skeletons, spinners, or meaningful partial UI.
- Scoped per segment: blog/loading.tsx only applies to /blog, not to /blog/[slug].
// app/dashboard/loading.tsx
// → automatically wraps app/dashboard/page.tsx in <Suspense>
export default function Loading() {
return (
<div className="space-y-4">
<Skeleton className="h-8 w-64" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
</div>
);
}Streaming with Suspenseintermediatestreaming
Wrap slow async components in <Suspense> to send the page shell instantly and stream the slow parts in when ready.
Builds onloading.tsx , Route-Level Streaming
Key Points
- <Suspense fallback={<Skeleton />}> shows the fallback until the async child resolves.
- Everything outside Suspense boundaries is part of the static shell , sent immediately.
- Multiple independent Suspense boundaries stream in parallel , one slow request never blocks others.
- React's use() hook: pass a server-initiated Promise to a Client Component and let Suspense handle the wait.
- loading.tsx is a shortcut , it's equivalent to wrapping page.tsx in a <Suspense> boundary.
Note
Textbook Definition
Streaming uses HTTP chunked transfer to progressively deliver HTML from server to client. React's Suspense boundaries define what can be deferred , the static shell arrives instantly while data-dependent chunks stream in as they resolve.
export default function Page() {
return (
<>
<StaticHeader /> {/* sent immediately */}
<Suspense fallback={<PostsSkeleton />}>
<BlogPosts /> {/* streams in independently */}
</Suspense>
<Suspense fallback={<CommentsSkeleton />}>
<Comments /> {/* streams in independently */}
</Suspense>
</>
);
}Warning
Gotcha
Nesting async components without Suspense creates a waterfall , if the parent awaits before rendering the child, the child's fetch can't start until the parent finishes. Wrap each independently-fetching component in its own Suspense.
"use cache" Directiveintermediatecaching
Add "use cache" to an async function or component to cache its output , composable SSG at any level of the tree.
Builds onStatic Site Generation (SSG)
Key Points
- "use cache" inside a function/component body caches its async output at data-level or UI-level.
- cacheLife("seconds" | "minutes" | "hours" | "days" | "weeks") , sets expiry duration.
- cacheTag("key") marks a cache entry; updateTag("key") from a Server Action invalidates it on demand.
- Arguments become cache keys automatically , different inputs → separate cache entries.
- Replaces the old { next: { revalidate } } fetch option with a composable, component-level model.
Note
Textbook Definition
"use cache" is a Next.js directive (requires cacheComponents: true in next.config.ts) that stores the return value of an async function or component. Function arguments automatically become part of the cache key, enabling parameterized caching.
import { cacheLife, cacheTag } from 'next/cache';
// Cache an individual data fetch
async function getProduct(id: string) {
'use cache';
cacheLife('hours');
cacheTag(`product-${id}`);
return db.products.findById(id);
}
// On-demand invalidation (Server Action)
async function updateProduct(id: string, data) {
'use server';
await db.products.update(id, data);
updateTag(`product-${id}`); // only this product's cache busted
}Warning
Gotcha
"use cache" requires cacheComponents: true in next.config.ts , without it, the directive is silently ignored. This is the most common reason caching appears not to work.
Partial Pre-Rendering (PPR)advancedppr
PPR is the default model with Cache Components , static shell served instantly from CDN, dynamic Suspense slots stream in per-request.
Key Points
- No extra configuration with Cache Components , PPR is the default rendering model.
- Static shell: everything outside dynamic Suspense boundaries pre-rendered at build time.
- Dynamic slots: Suspense fallback HTML is included in the static shell; real content streams at request time.
- Same URL can serve static nav, cached blog posts, AND per-user recommendations.
- Achieves TTFB as fast as pure static hosting while supporting fully dynamic personalized sections.
Note
Textbook Definition
PPR combines static and dynamic rendering on a single page without compromise. At build time, Next.js pre-renders everything that can be static (cached + deterministic) into an HTML shell. At request time, dynamic Suspense boundaries stream in personalized content.
// One page , three rendering modes simultaneously
export default function ProductPage() {
return (
<>
{/* Static — pre-rendered at build, served from CDN */}
<StaticNav />
{/* Cached — in static shell, revalidates periodically */}
<CachedReviews />
{/* Dynamic — streams in per-request (personalized) */}
<Suspense fallback={<RecommendationsSkeleton />}>
<PersonalizedRecommendations />
</Suspense>
</>
);
}CSR vs SSR vs Pre-renderingbasicrendering
CSR renders in the browser; SSR/SSG pre-render HTML on the server for faster first paint and SEO.
Builds onStatic vs Dynamic Rendering
Key Points
- CSR: the browser downloads JS, then renders , first paint is an empty shell (poor SEO).
- Pre-rendering: Next.js generates HTML ahead of time so content is visible immediately.
- Two pre-render modes: SSG (at build time) and SSR (per request).
- After HTML arrives, React HYDRATES it , attaching event listeners to make it interactive.
- Pre-rendering improves first contentful paint, SEO, and perceived performance.
// CSR: <div id="root"></div> → JS fills it in the browser // SSR/SSG: server sends full HTML → React hydrates it // Pre-rendered page is visible (and crawlable) before JS loads
Automatic Static Optimizationintermediaterendering
Pages Router auto-prerenders pages with no server-side data needs into static HTML at build time.
Builds onPages Router & Special Files
Key Points
- During build, Next.js analyzes each Pages-Router page for data dependencies.
- A page WITHOUT getServerSideProps / getInitialProps is auto-optimized to static HTML.
- Such pages serve instantly with no per-request server render.
- Pages using only client state, context, or client libraries qualify.
- For freshness, combine with ISR (revalidate) so static pages update over time.
// Auto-statically-optimized , no data functions
export default function About() {
return <h1>About us</h1>; // prerendered to static HTML at build
}
// Opts OUT of static optimization → SSR every request
export async function getServerSideProps() {
return { props: {} };
}Warning
Gotcha
Adding getServerSideProps (even returning nothing useful) opts a page OUT of static optimization, forcing SSR on every request. Only add it when you truly need per-request data.
getStaticProps (SSG)intermediatepages-router
Pages Router data fetch that runs at build time and feeds props into a statically generated page.
Builds onAutomatic Static Optimization
Key Points
- Runs ONCE at build time (or during ISR revalidation) , never on the client.
- Returns { props } that are passed to the page component.
- Add revalidate: n to enable ISR , the page regenerates at most every n seconds.
- Ideal for content that is the same for all users (blogs, docs, marketing).
- Pair with getStaticPaths for dynamic routes ([id]).
export async function getStaticProps() {
const data = await fetchData();
return {
props: { data },
revalidate: 60 // ISR: regenerate at most once per minute
};
}
export default function Page({ data }) {
return <List items={data} />;
}Warning
Gotcha
getStaticProps runs only on the server at build , it is stripped from the client bundle, so you can safely use secrets and direct DB calls inside it.
getServerSideProps (SSR)intermediatepages-router
Pages Router data fetch that runs on every request, rendering the page server-side with fresh data.
Builds onAutomatic Static Optimization
Key Points
- Runs on EVERY request , always fresh, can read req/res, cookies, headers.
- Returns { props } computed server-side and passed to the page.
- Use for personalized or frequently-changing data (dashboards, authed pages).
- Slower than SSG: adds server latency per request (no CDN caching of HTML).
- Can return { redirect } or { notFound: true } to control the response.
export async function getServerSideProps(context) {
const { req, params } = context;
const user = await getUser(req.cookies.token);
if (!user) return { redirect: { destination: '/login', permanent: false } };
return { props: { user } }; // fresh on every request
}Warning
Gotcha
getServerSideProps disables static optimization for that route. If the data does not actually change per request, use getStaticProps + ISR instead for far better performance.
getStaticPaths & fallbackintermediatepages-router
Declares which dynamic routes to pre-render at build time; fallback controls un-listed paths.
Builds ongetStaticProps (SSG)
Key Points
- Used with getStaticProps for dynamic routes (pages/blog/[id].js).
- Returns { paths, fallback } , paths is the list to pre-render at build time.
- fallback: false , any path not listed returns a 404.
- fallback: true , serve a fallback page, then generate in the background and cache.
- fallback: 'blocking' , SSR the page on first request, then cache it (no fallback UI).
- App Router equivalent: generateStaticParams (no fallback option , uses dynamicParams).
export async function getStaticPaths() {
const posts = await getPosts();
return {
paths: posts.map((p) => ({ params: { id: p.id } })),
fallback: 'blocking' // un-listed ids: SSR once, then cache
};
}
export async function getStaticProps({ params }) {
const post = await getPost(params.id);
return { props: { post } };
}Warning
Gotcha
With fallback: true the page first renders with no data , you must handle router.isFallback and show a loading state, or it crashes accessing undefined props.