Understanding rendering strategies is the key to building fast, scalable Next.js applications. Each strategy determines when and where your content is generated.
Two Rendering Strategies:
Static rendering generates HTML at build time. The result is cached and served directly from the CDN, providing the fastest possible response times.
// app/about/page.tsx
// This page is static by default - no data fetching
export default function AboutPage() {
return (
<div>
<h1>About Our Company</h1>
<p>Founded in 2020, we build amazing products.</p>
</div>
)
}Static rendering benefits:
Dynamic rendering generates HTML on every request. This ensures users always see the most current data, but requires server compute time.
What triggers dynamic rendering:
cookies() or headers()searchParams prop// app/dashboard/page.tsx
import { cookies } from 'next/headers'
export default async function Dashboard() {
// Reading cookies makes this page dynamic
const session = (await cookies()).get('session')
if (!session) {
redirect('/login')
}
const userData = await getUserData(session.value)
return (
<div>
<h1>Welcome, {userData.name}</h1>
<p>Your personalized dashboard</p>
</div>
)
}Add revalidation to static rendering to get automatic background updates. Pages are served instantly from cache, then automatically regenerated after a specified time period.
// app/products/page.tsx
// Static page with 60-second revalidation
export const revalidate = 60 // Revalidate every 60 seconds
export default async function ProductsPage() {
const products = await fetch('https://api.example.com/products')
.then(res => res.json())
return (
<div>
<h1>Products</h1>
<p>Last updated: {new Date().toLocaleTimeString()}</p>
{products.map(product => (
<div key={product.id}>{product.name}</div>
))}
</div>
)
}Revalidation timeline:
Instead of waiting for time-based revalidation, you can trigger cache updates immediately when content changes using revalidatePath() or revalidateTag().
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
export async function updatePost(id: string, data: FormData) {
await db.post.update({
where: { id },
data: {
title: data.get('title'),
content: data.get('content'),
}
})
// Invalidate the specific post page
revalidatePath(`/blog/${id}`)
// Also invalidate the blog listing
revalidatePath('/blog')
}Choosing between path and tag:
| Method | Use When | Example |
|---|---|---|
revalidatePath | You know the exact URL to invalidate | Single page update |
revalidateTag | Data is shared across multiple pages | Product data on listing + detail pages |
Certain functions and patterns automatically make your page dynamic. Understanding these "dynamic signals" is crucial for controlling your rendering strategy.
| Function | Trigger | Alternative |
|---|---|---|
cookies() | Always dynamic | Pass as prop from parent |
headers() | Always dynamic | Use proxy/middleware for auth |
searchParams | Always dynamic | Use generateStaticParams for known values |
// This page is accidentally dynamic!
export default async function ProductPage({ params }) {
const { id } = await params
// Reading headers makes the ENTIRE page dynamic
const locale = (await headers()).get('accept-language')
const product = await getProduct(id)
return <div>...</div>
}
// Better: Extract dynamic part
export default async function ProductPage({ params }) {
const { id } = await params
const product = await getProduct(id) // Can be cached
return (
<div>
<ProductDetails product={product} />
{/* Only this component is dynamic */}
<Suspense fallback={<div>Loading...</div>}>
<LocalizedContent />
</Suspense>
</div>
)
}
async function LocalizedContent() {
const locale = (await headers()).get('accept-language')
return <div>Content in {locale}</div>
}For dynamic routes like /blog/[slug], use generateStaticParams to pre-render pages at build time instead of generating them on-demand.
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getAllPosts()
return posts.map(post => ({
slug: post.slug,
}))
}
// Now each post page is pre-rendered at build time
export default async function BlogPost({ params }) {
const { slug } = await params
const post = await getPost(slug)
return <article>{post.content}</article>
}dynamicParams options:
Streaming allows you to progressively send HTML to the client. Fast content appears immediately while slower content loads in the background.
import { Suspense } from 'react'
export default function DashboardPage() {
return (
<div>
{/* Renders immediately - no data dependency */}
<h1>Dashboard</h1>
<nav>Overview | Analytics | Settings</nav>
{/* Fast data - appears quickly */}
<Suspense fallback={<StatsSkeleton />}>
<QuickStats />
</Suspense>
{/* Medium speed data */}
<Suspense fallback={<ChartSkeleton />}>
<AnalyticsChart />
</Suspense>
{/* Slow data - loads last */}
<Suspense fallback={<TableSkeleton />}>
<DetailedReport />
</Suspense>
</div>
)
}
async function QuickStats() {
const stats = await getStats() // ~100ms
return <StatsCards data={stats} />
}
async function AnalyticsChart() {
const data = await getChartData() // ~500ms
return <Chart data={data} />
}
async function DetailedReport() {
const report = await generateReport() // ~2000ms
return <ReportTable data={report} />
}Benefits of streaming:
unstable_cache wraps any async function and persists its return value in Next.js's Data Cache. This API enables caching for database queries, SDK calls, and expensive computations.
import { unstable_cache } from 'next/cache'
const cachedFn = unstable_cache(
fetchData, // () => Promise<T> - the function to cache
keyParts?, // string[] - extra cache-key segments
options? // { tags?: string[]; revalidate?: number | false }
)Common Use Cases:
cookies(), headers()) inside the cached function. Inputs and outputs must be JSON-serializable.Use this decision tree to choose the optimal rendering strategy for your page:
| Content Type | Strategy | Example |
|---|---|---|
| Never changes | Static | Legal pages, about page |
| Changes periodically | Static + revalidation | Blog posts, product listings |
| User-specific data | Dynamic (in Suspense) | Dashboard, cart, profile |
| Real-time requirements | Dynamic | Live scores, stock prices |
Content that rarely changes should be fully static
| Strategy | When Generated | Revalidation | Use For |
|---|---|---|---|
| Static | Build time | Optional (time-based or on-demand) | Marketing, blog, products |
| Dynamic | Every request | N/A | Personalized, real-time |
Key APIs:
| API | Purpose |
|---|---|
revalidate | Route segment config for revalidation timing |
revalidatePath() | Invalidate cache by URL |
revalidateTag() | Invalidate cache by tag |
generateStaticParams() | Pre-render dynamic routes |
unstable_cache() | Cache any async function |
connection() | Opt into dynamic rendering (replaces unstable_noStore) |