rendering methods
rendering methods
next.js gives you four ways to render pages. each has its use case.
CSR (client-side rendering)
javascript renders in the browser after page loads.
flow:
- server sends minimal HTML + JS bundles
- browser downloads & executes JS
- JS fetches data from APIs
- react renders
- page becomes interactive
use for:
- dashboards with frequent interactions
- admin panels requiring auth
- real-time apps with live data
- interactive tools/calculators
- private content (no SEO needed)
import { useState, useEffect } from 'react';
export default function Dashboard() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchData() {
const res = await fetch('/api/user/profile');
const json = await res.json();
setData(json);
setLoading(false);
}
fetchData();
}, []);
if (loading) return <div>loading...</div>;
return <div>welcome, {data.name}!</div>;
}
pros:
- less server load
- great for dynamic content
- smooth client transitions
cons:
- slow initial load
- poor SEO
- blank page while loading
SSR (server-side rendering)
page rendered on server for each request.
flow:
- request hits server
- server fetches data
- server renders HTML
- browser receives full HTML
- JS hydrates for interactivity
use for:
- personalized content (user dashboards)
- frequently updated data
- SEO-critical pages with dynamic data
- pages needing fresh data on every visit
export default function Profile({ user }) {
return (
<div>
<h1>{user.name}</h1>
<p>last login: {user.lastLogin}</p>
</div>
);
}
export async function getServerSideProps(context) {
const { userId } = context.params;
const res = await fetch(`https://api.example.com/users/${userId}`);
const user = await res.json();
return { props: { user } };
}
pros:
- good SEO
- fast first contentful paint
- always fresh data
cons:
- higher server load
- slower than static
- can't cache easily
SSG (static site generation)
pages generated at build time.
flow:
- build process fetches data
- generates static HTML files
- files served from CDN
- instant page loads
use for:
- blog posts
- marketing pages
- documentation
- product pages
- content that doesn't change often
export default function BlogPost({ post }) {
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
);
}
export async function getStaticProps({ params }) {
const post = await getPost(params.slug);
return { props: { post } };
}
export async function getStaticPaths() {
const posts = await getAllPosts();
const paths = posts.map(post => ({
params: { slug: post.slug }
}));
return { paths, fallback: false };
}
pros:
- blazing fast
- great SEO
- low server cost
- can use CDN
cons:
- data can be stale
- long build times with many pages
- need rebuild for updates
ISR (incremental static regeneration)
static pages that update periodically.
flow:
- serve static page
- check if revalidation time passed
- regenerate in background if needed
- serve fresh page on next request
use for:
- e-commerce product pages
- news sites
- content that updates occasionally
- high-traffic pages needing some freshness
export default function Product({ product }) {
return (
<div>
<h1>{product.name}</h1>
<p>price: ${product.price}</p>
<p>stock: {product.stock}</p>
</div>
);
}
export async function getStaticProps({ params }) {
const product = await getProduct(params.id);
return {
props: { product },
revalidate: 60 // regenerate every 60 seconds
};
}
export async function getStaticPaths() {
return {
paths: [],
fallback: 'blocking' // or true
};
}
pros:
- fast like SSG
- data stays relatively fresh
- scales well
- good SEO
cons:
- can serve stale data briefly
- complex caching behavior
- fallback handling needed
fallback options
fallback: false
- only pre-rendered paths work
- 404 for others
fallback: true
- shows fallback UI while generating
- page generated on first request
export default function Page({ data }) {
const router = useRouter();
if (router.isFallback) {
return <div>loading...</div>;
}
return <div>{data.title}</div>;
}
fallback: 'blocking'
- waits for page generation
- no fallback UI needed
- better for SEO
on-demand revalidation
trigger revalidation manually:
// pages/api/revalidate.js
export default async function handler(req, res) {
const { secret, path } = req.query;
// check secret
if (secret !== process.env.REVALIDATE_SECRET) {
return res.status(401).json({ message: 'invalid token' });
}
try {
await res.revalidate(path);
return res.json({ revalidated: true });
} catch (err) {
return res.status(500).send('error revalidating');
}
}
trigger it:
curl https://yoursite.com/api/revalidate?secret=YOUR_SECRET&path=/blog/post-1
mixing strategies
you can use different methods for different pages:
/ → SSG (homepage)
/blog → SSG (blog list)
/blog/[slug] → ISR (blog posts)
/dashboard → CSR (user dashboard)
/profile/[id] → SSR (user profiles)
/products/[id] → ISR (product pages)
quick comparison
| method | when rendered | data freshness | SEO | speed | server load | |--------|--------------|----------------|-----|-------|-------------| | CSR | browser | always fresh | poor | slow initial | low | | SSR | each request | always fresh | great | medium | high | | SSG | build time | stale | great | fastest | lowest | | ISR | build + periodic | mostly fresh | great | fast | low |
choosing the right method
use CSR if:
- no SEO needed
- highly interactive
- user-specific data
- real-time updates
use SSR if:
- need SEO
- data changes constantly
- personalized content
- can handle server load
use SSG if:
- content rarely changes
- SEO critical
- want max performance
- ok with rebuilds
use ISR if:
- content updates occasionally
- need speed + freshness balance
- high traffic
- SEO matters
performance tips
- preload critical data
- optimize images with next/image
- lazy load components
- use SWR for client-side caching
- implement proper loading states
- minimize JS bundles
- use CDN for static assets
- monitor core web vitals