Rendering Strategies
Static, dynamic, streaming, and partial prerendering explained
Rendering in Next.js
Next.js provides multiple rendering strategies to optimize performance and user experience. Understanding when to use each strategy is key to building fast, scalable applications.
🎨 Rendering Strategies
SSG - Static Site Generation (build time)
SSR - Server-Side Rendering (request time)
ISR - Incremental Static Regeneration
CSR - Client-Side Rendering
Streaming - Progressive rendering
PPR - Partial Prerendering (experimental)
Static Site Generation (SSG)
// Pages are rendered at BUILD time
// Best for: blogs, marketing pages, documentation
// app/posts/page.tsx
export default async function Posts() {
const posts = await getPosts(); // Fetched at build time
return (
<ul>
{posts.map(post => <li key={post.id}>{post.title}</li>)}
</ul>
);
}
// Generate static pages for dynamic routes
// app/posts/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map(post => ({
slug: post.slug,
}));
}
export default async function Post({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
return <article>{post.content}</article>;
}
// This generates HTML at build time for each post
Server-Side Rendering (SSR)
// Pages are rendered on EACH REQUEST
// Best for: personalized content, real-time data
// Force dynamic rendering
export const dynamic = 'force-dynamic';
// Or use no-store in fetch
async function getUser() {
const res = await fetch('/api/user', { cache: 'no-store' });
return res.json();
}
// Using cookies/headers makes it dynamic automatically
import { cookies, headers } from 'next/headers';
export default async function Dashboard() {
const cookieStore = await cookies();
const token = cookieStore.get('auth-token');
const headersList = await headers();
const userAgent = headersList.get('user-agent');
const user = await getUser(token);
return <div>Welcome, {user.name}</div>;
}
Incremental Static Regeneration (ISR)
// Static pages that revalidate periodically
// Best for: e-commerce, news sites, frequently updated content
// Time-based revalidation
export const revalidate = 60; // Revalidate every 60 seconds
export default async function Products() {
const products = await fetch('https://api.example.com/products', {
next: { revalidate: 60 }, // Or per-fetch
}).then(r => r.json());
return <ProductGrid products={products} />;
}
// On-demand revalidation
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';
export async function POST(request: Request) {
const { path, tag } = await request.json();
if (path) revalidatePath(path);
if (tag) revalidateTag(tag);
return Response.json({ revalidated: true, now: Date.now() });
}
Streaming with Suspense
import { Suspense } from 'react';
// Stream content progressively
export default function Page() {
return (
<div>
{/* Renders immediately */}
<header>
<h1>Dashboard</h1>
</header>
{/* Each section streams independently */}
<div className="grid grid-cols-3 gap-4">
<Suspense fallback={<CardSkeleton />}>
<RevenueCard /> {/* 100ms */}
</Suspense>
<Suspense fallback={<CardSkeleton />}>
<UsersCard /> {/* 200ms */}
</Suspense>
<Suspense fallback={<CardSkeleton />}>
<OrdersCard /> {/* 500ms */}
</Suspense>
</div>
{/* Slow component doesn't block others */}
<Suspense fallback={<TableSkeleton />}>
<DataTable /> {/* 2000ms */}
</Suspense>
</div>
);
}
// Each component can be an async Server Component
async function RevenueCard() {
const revenue = await getRevenue();
return <Card>Revenue: {revenue}</Card>;
}
Loading.tsx for Route-level Loading
// app/dashboard/loading.tsx
// Automatically wraps page in Suspense
export default function Loading() {
return (
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-1/4 mb-4" />
<div className="grid grid-cols-3 gap-4">
<div className="h-32 bg-gray-200 rounded" />
<div className="h-32 bg-gray-200 rounded" />
<div className="h-32 bg-gray-200 rounded" />
</div>
</div>
);
}
// File structure
app/
├── dashboard/
│ ├── loading.tsx # Shows while page loads
│ └── page.tsx # The actual page
Partial Prerendering (PPR)
// Experimental: Static shell + dynamic holes
// Enable in next.config.js
// next.config.js
const nextConfig = {
experimental: {
ppr: true,
},
};
// app/page.tsx
import { Suspense } from 'react';
export default function Page() {
return (
<div>
{/* Static shell - prerendered */}
<Header />
<Hero />
{/* Dynamic hole - rendered on request */}
<Suspense fallback={<CartSkeleton />}>
<Cart /> {/* Uses cookies/auth */}
</Suspense>
{/* Static content */}
<FeaturedProducts />
<Footer />
</div>
);
}
// PPR serves static shell instantly from CDN
// Then streams in dynamic parts
Choosing a Strategy
| Use Case | Strategy | Example |
|---|---|---|
| Static content | SSG | Blog, docs |
| Personalized data | SSR | Dashboard, settings |
| Frequently updated | ISR | E-commerce, news |
| Mixed static/dynamic | PPR | Landing + cart |
| Fast initial load | Streaming | Complex dashboards |
✅ Rendering Best Practices
- • Default to static - add dynamism only when needed
- • Use Suspense boundaries for independent loading
- • Choose ISR for content that changes periodically
- • Use streaming for slow data sources
- • Add loading.tsx for route-level loading states
- • Consider PPR for pages with mixed static/dynamic content