Next.js renders pages in different ways to optimize performance. Static Rendering (SSG) pre-generates HTML at build time for lightning-fast loads. Dynamic Rendering (SSR) generates HTML on each request for personalized content. Incremental Static Regeneration (ISR) combines bothβstatic with periodic updates. Understanding when and how pages render is crucial for building fast, scalable applications!
Three Types of Rendering
1. Static Rendering (SSG - Static Site Generation)
- When: Build time
- Caching: Served from CDN indefinitely
- Use for: Marketing pages, blog posts, documentation
- Speed: β‘ Fastest (pre-rendered HTML)
2. Dynamic Rendering (SSR - Server-Side Rendering)
- When: Request time (every request)
- Caching: No caching (always fresh)
- Use for: User dashboards, personalized content, real-time data
- Speed: π’ Slower (rendered on demand)
3. Incremental Static Regeneration (ISR)
- When: Build time + periodic revalidation
- Caching: Cached with time-based revalidation
- Use for: E-commerce products, news articles, CMS content
- Speed: β‘ Fast (static) + π Updated (revalidated)
Next.js Default Behavior
Next.js defaults to Static Rendering whenever possible for maximum performance. A route automatically becomes dynamic if it uses dynamic functions (cookies, headers, searchParams) or opts out of caching.
Static Rendering (SSG)
Fully Static Page
// This page is FULLY STATIC
export default function AboutPage() {
return (
<div>
<h1>About Us</h1>
<p>We are a company that builds amazing things.</p>
<p>Founded in 2024.</p>
</div>
);
}
// β
Rendered at build time
// β
HTML cached indefinitely
// β
Served from CDN
// β
Fastest possible load time
// β
No server processing on requestStatic with Cached Data
// Static page with data fetching
export default async function BlogPage() {
// Default: cached indefinitely
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return (
<div>
<h1>Blog</h1>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
);
}
// β
Rendered at build time
// β
Fetch data once during build
// β
HTML + data cached
// β
No data fetching on requestExplicitly Static
export const dynamic = 'force-static'; // Force static rendering
export default async function DocsPage() {
const docs = await fetch('https://api.example.com/docs', {
cache: 'force-cache', // Explicitly cache
}).then(r => r.json());
return (
<div>
<h1>Documentation</h1>
{/* Render docs */}
</div>
);
}
// β
Forced to be static
// β
Even if it uses dynamic-like patterns
// β
Good for ensuring pages are always staticWhen Pages Are Static
β Pages are Static when:
- No dynamic functions (cookies, headers, searchParams)
- All data fetching uses default caching (cache: 'force-cache')
- No dynamic segments without generateStaticParams
- export const dynamic = 'force-static' is set
Dynamic Rendering (SSR)
Using Dynamic Functions
import { cookies } from 'next/headers';
// This page is DYNAMIC because it uses cookies()
export default async function DashboardPage() {
const cookieStore = cookies();
const userId = cookieStore.get('userId')?.value;
// Fetch user-specific data
const userData = await fetch(`https://api.example.com/users/${userId}`).then(r => r.json());
return (
<div>
<h1>Welcome, {userData.name}!</h1>
<p>Your personal dashboard</p>
</div>
);
}
// β
Rendered on every request
// β
Fresh user data every time
// β
Personalized content
// β οΈ Slower than static (server processing)Using headers()
import { headers } from 'next/headers';
// Dynamic because of headers()
export default async function ApiDocsPage() {
const headersList = headers();
const userAgent = headersList.get('user-agent');
// Customize based on device
const isMobile = /Mobile/i.test(userAgent || '');
return (
<div>
<h1>API Documentation</h1>
{isMobile ? <MobileDocs /> : <DesktopDocs />}
</div>
);
}
// β
Personalized based on request headers
// β
Different content per device
// β
Dynamic renderingUsing searchParams
// Dynamic because of searchParams
export default async function SearchPage({
searchParams,
}: {
searchParams: { q?: string; page?: string };
}) {
const query = searchParams.q || '';
const page = parseInt(searchParams.page || '1', 10);
// Search based on query parameters
const results = await fetch(
`https://api.example.com/search?q=${query}&page=${page}`
).then(r => r.json());
return (
<div>
<h1>Search Results for "{query}"</h1>
{results.map(result => (
<div key={result.id}>{result.title}</div>
))}
</div>
);
}
// β
Different content per query
// β
Dynamic based on URL params
// β
Rendered on requestOpting Out of Caching
// Dynamic because of cache: 'no-store'
export default async function LiveDataPage() {
const data = await fetch('https://api.example.com/live', {
cache: 'no-store', // Don't cache, always fetch fresh
}).then(r => r.json());
return (
<div>
<h1>Live Data</h1>
<p>Updated: {new Date().toLocaleTimeString()}</p>
<p>Value: {data.value}</p>
</div>
);
}
// β
Fresh data on every request
// β
No caching
// β
Dynamic renderingExplicitly Dynamic
export const dynamic = 'force-dynamic'; // Force dynamic rendering
export default async function DashboardPage() {
// Even without dynamic functions, this is dynamic
const data = await fetch('https://api.example.com/data').then(r => r.json());
return <div>{data.content}</div>;
}
// β
Forced to be dynamic
// β
Rendered on every request
// β
Never cachedWhen Pages Are Dynamic
β οΈ Pages are Dynamic when:
- Using cookies(), headers(), or searchParams
- Using cache: 'no-store' in fetch
- Using revalidate: 0 in fetch
- export const dynamic = 'force-dynamic' is set
- Dynamic segments without generateStaticParams
Incremental Static Regeneration (ISR)
Time-Based Revalidation
// ISR: Static with revalidation every hour
export default async function BlogPostPage({
params,
}: {
params: { slug: string };
}) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`, {
next: { revalidate: 3600 }, // Revalidate every 1 hour (3600 seconds)
}).then(r => r.json());
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
<p>Published: {post.publishedAt}</p>
</article>
);
}
// β
Generated statically at build
// β
Served from cache for 1 hour
// β
After 1 hour, regenerates in background
// β
Subsequent requests get updated version
// β
Fast + fresh dataRoute Segment Config
// Revalidate entire route segment
export const revalidate = 3600; // 1 hour
export default async function ProductsPage() {
const products = await fetch('https://api.example.com/products').then(r => r.json());
return (
<div>
<h1>Products</h1>
{products.map(product => (
<div key={product.id}>
<h2>{product.name}</h2>
<p>${product.price}</p>
</div>
))}
</div>
);
}
// β
Applies to entire route
// β
All fetches in this route use same revalidation
// β
Simpler than per-fetch revalidationDifferent Revalidation Times
export default async function DashboardPage() {
// Revalidate every 60 seconds
const stats = await fetch('https://api.example.com/stats', {
next: { revalidate: 60 },
}).then(r => r.json());
// Revalidate every 10 minutes
const notifications = await fetch('https://api.example.com/notifications', {
next: { revalidate: 600 },
}).then(r => r.json());
// Never cache (always fresh)
const liveData = await fetch('https://api.example.com/live', {
cache: 'no-store',
}).then(r => r.json());
return (
<div>
<h1>Dashboard</h1>
<Stats data={stats} />
<Notifications data={notifications} />
<LiveFeed data={liveData} />
</div>
);
}
// β
Different revalidation per data source
// β
Optimize based on update frequency
// β
Mix static, ISR, and dynamic dataISR with generateStaticParams
// Generate static paths at build time
export async function generateStaticParams() {
const products = await fetch('https://api.example.com/products').then(r => r.json());
return products.map((product) => ({
id: product.id,
}));
}
// ISR for each product
export default async function ProductPage({
params,
}: {
params: { id: string };
}) {
const product = await fetch(`https://api.example.com/products/${params.id}`, {
next: { revalidate: 3600 }, // Revalidate every hour
}).then(r => r.json());
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p>${product.price}</p>
</div>
);
}
// β
Pre-generate known products at build
// β
Each product revalidates independently
// β
New products generated on-demand
// β
Fast + always up-to-dateRendering Strategies by Route
Different rendering strategies for different routes
Select a file or folder to see details
Rendering Comparison
| Feature | Static (SSG) | Dynamic (SSR) | ISR |
|---|---|---|---|
| When Rendered | Build time | Request time | Build + revalidation |
| Performance | β‘ Fastest | π’ Slower | β‘ Fast |
| Data Freshness | Stale until rebuild | Always fresh | Periodic updates |
| Caching | Indefinite CDN | No caching | CDN with TTL |
| Server Load | None (cached) | High (every request) | Low (periodic) |
| Best For | Marketing, docs, blog | Dashboards, personalized | E-commerce, news, CMS |
| Build Time | Increases with pages | Fast builds | Build known paths |
Cache Control Options
Force Cache (Static)
// Force caching - always static
const data = await fetch('https://api.example.com/data', {
cache: 'force-cache', // Default behavior
});
// β
Cached at build time
// β
Never refetches
// β
Fastest optionNo Store (Dynamic)
// Never cache - always dynamic
const data = await fetch('https://api.example.com/data', {
cache: 'no-store', // Opt out of caching
});
// β
Fresh data every request
// β
Dynamic rendering
// β
Use for real-time dataRevalidate (ISR)
// Cache with revalidation - ISR
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 60 }, // Revalidate every 60 seconds
});
// β
Static + periodic updates
// β
Best of both worlds
// β
Use for frequently updated contentNo Revalidate (Static Forever)
// Cache forever
const data = await fetch('https://api.example.com/data', {
next: { revalidate: false }, // Or omit revalidate
});
// β
Cached at build
// β
Never revalidates
// β
Use for truly static contentRevalidate 0 (Dynamic)
// Revalidate immediately - effectively dynamic
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 0 },
});
// β
Same as cache: 'no-store'
// β
Always fresh
// β
Dynamic renderingRoute Segment Config Options
dynamic
// Control rendering mode
export const dynamic = 'auto'; // Default: auto-detect
// export const dynamic = 'force-static'; // Always static
// export const dynamic = 'force-dynamic'; // Always dynamic
// export const dynamic = 'error'; // Error if dynamic
export default function Page() {
return <div>Content</div>;
}
// 'auto': Next.js decides based on usage
// 'force-static': Force static even with dynamic functions
// 'force-dynamic': Force dynamic even without dynamic functions
// 'error': Throw error if page becomes dynamicrevalidate
// Set revalidation for entire route
export const revalidate = 3600; // 1 hour
// export const revalidate = 60; // 1 minute
// export const revalidate = false; // Never revalidate
// export const revalidate = 0; // Revalidate on every request (dynamic)
export default async function BlogPage() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return <div>{/* posts */}</div>;
}
// Applies to all data fetching in this route
// Can be overridden per-fetch with next.revalidatedynamicParams
// Control behavior for dynamic params not in generateStaticParams
export const dynamicParams = true; // Default: generate on-demand
// export const dynamicParams = false; // Return 404 if not pre-generated
export async function generateStaticParams() {
// Generate params for known products
return [
{ id: '1' },
{ id: '2' },
{ id: '3' },
];
}
export default function ProductPage({ params }: { params: { id: string } }) {
return <div>Product {params.id}</div>;
}
// dynamicParams = true: New products generated on first visit
// dynamicParams = false: Only pre-generated products existRendering Best Practices
1. Default to Static
// β
GOOD: Let Next.js optimize
export default async function Page() {
const data = await fetch('https://api.example.com/data');
return <div>{/* render */}</div>;
}
// β BAD: Forcing dynamic unnecessarily
export const dynamic = 'force-dynamic';
export default function Page() {
// Could be static but forced dynamic
return <div>Static content</div>;
}
// Use static rendering by default for best performance2. Use ISR for Semi-Dynamic Content
// β
GOOD: ISR for content that changes occasionally
export const revalidate = 3600; // 1 hour
export default async function ProductsPage() {
const products = await fetch('https://api.example.com/products').then(r => r.json());
return <div>{/* products */}</div>;
}
// β BAD: Dynamic for content that rarely changes
export const dynamic = 'force-dynamic';
// Causes unnecessary server load
// ISR provides fast loads + fresh content3. Reserve Dynamic for Personalized Content
// β
GOOD: Dynamic for user-specific data
import { cookies } from 'next/headers';
export default async function DashboardPage() {
const userId = cookies().get('userId')?.value;
const userData = await fetch(`/api/users/${userId}`).then(r => r.json());
return <div>Welcome, {userData.name}</div>;
}
// Dynamic rendering justified for personalized content4. Choose Appropriate Revalidation Times
// β
GOOD: Match revalidation to update frequency
// Blog posts: 1 hour (rarely change)
export const revalidate = 3600;
// Product prices: 5 minutes (change frequently)
export const revalidate = 300;
// News articles: 1 minute (very frequent)
export const revalidate = 60;
// Static content: never
export const revalidate = false;
// Match revalidation to your content update frequency5. Use generateStaticParams for Known Routes
// β
GOOD: Pre-generate known routes
export async function generateStaticParams() {
const products = await fetch('https://api.example.com/products').then(r => r.json());
return products.map(p => ({ id: p.id }));
}
// Faster builds, better performance for known routes
// β BAD: All routes generated on-demand
// Slower first visits, increased server load
// Pre-generate what you know, on-demand for the restKey Takeaways
- Static (SSG) - build time, cached forever, fastest
- Dynamic (SSR) - request time, always fresh, personalized
- ISR - static + periodic updates, best of both
- Default to static - Next.js optimizes automatically
- cache: 'no-store' - opts into dynamic rendering
- revalidate - time-based ISR (seconds)
- Dynamic functions - cookies(), headers(), searchParams trigger dynamic
- Choose wisely - match rendering to content type
What's Next?
You've mastered static and dynamic rendering! Next, we'll explore generateStaticParams for Static Generationβpre-generating dynamic routes at build time, controlling which paths to generate, implementing fallback behavior, and optimizing build times. You'll create lightning-fast static sites with dynamic routes!
We'll cover generateStaticParams, dynamicParams, fallback behavior, and complete static generation strategies.
β‘ Performance Tip
Choose the right rendering strategy: Static for marketing pages and blogs (fastest), ISR for e-commerce and news (fast + fresh), Dynamic for dashboards and personalized content (always current). Match your rendering to your content update frequency!