I’ve built web apps using every rendering strategy out there. Here’s what I learned about when to use each one.
The Problem with Wrong Rendering Choices
Last year, I built an e-commerce site using pure CSR. Big mistake. Google couldn’t index product pages, SEO was terrible, and initial load times were 3+ seconds. I had to rebuild the entire thing with SSR.
Choosing the wrong rendering strategy costs time, money, and users.
Client-Side Rendering (CSR)
What it is: JavaScript renders everything in the browser after downloading.
When I use it:
- Admin dashboards
- Internal tools
- Apps behind authentication
- Heavy interactive features
Real Implementation
// Dashboard with real-time data
function AdminDashboard() {
const [metrics, setMetrics] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Fetch data after component mounts
fetchDashboardData()
.then(data => {
setMetrics(data);
setLoading(false);
});
// Set up real-time updates
const interval = setInterval(fetchDashboardData, 30000);
return () => clearInterval(interval);
}, []);
if (loading) return <DashboardSkeleton />;
return (
<div>
<MetricsCards data={metrics} />
<RealtimeChart data={metrics.chartData} />
<UserTable users={metrics.users} />
</div>
);
}
Pros:
- Fast navigation between pages
- Rich interactions and animations
- Reduced server load
- Great for SPAs
Cons:
- Poor SEO (search engines struggle)
- Slow initial load
- Requires JavaScript to work
- Large bundle sizes
Performance: 2-4 second initial load, then instant navigation.
Server-Side Rendering (SSR)
What it is: Server generates HTML for each request, sends complete page to browser.
When I use it:
- E-commerce product pages
- Blog posts
- Marketing landing pages
- Anything that needs SEO
Real Implementation
// Next.js product page
export async function getServerSideProps({ params }) {
// This runs on every request
const product = await fetchProduct(params.id);
const reviews = await fetchReviews(params.id);
const relatedProducts = await fetchRelatedProducts(product.category);
if (!product) {
return { notFound: true };
}
return {
props: {
product,
reviews,
relatedProducts
}
};
}
function ProductPage({ product, reviews, relatedProducts }) {
return (
<div>
<ProductDetails product={product} />
<ReviewsList reviews={reviews} />
<RelatedProducts products={relatedProducts} />
</div>
);
}
Pros:
- Excellent SEO
- Fast initial page load
- Works without JavaScript
- Good for dynamic content
Cons:
- Slower navigation
- Higher server costs
- Increased TTFB (Time to First Byte)
- Server needs to handle all requests
Performance: 500ms-1s initial load, 300-800ms navigation.
Static Site Generation (SSG)
What it is: Pages are pre-built at build time, served as static files.
When I use it:
- Documentation sites
- Portfolios
- Landing pages
- Content that doesn’t change often
Real Implementation
// Blog post page
export async function getStaticProps({ params }) {
// This runs at build time
const post = await getPostBySlug(params.slug);
const relatedPosts = await getRelatedPosts(post.tags);
return {
props: {
post,
relatedPosts
},
// Regenerate page every 24 hours
revalidate: 86400
};
}
export async function getStaticPaths() {
// Pre-generate paths for popular posts
const popularPosts = await getPopularPosts(100);
return {
paths: popularPosts.map(post => ({
params: { slug: post.slug }
})),
// Generate other pages on-demand
fallback: 'blocking'
};
}
function BlogPost({ post, relatedPosts }) {
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
<RelatedPosts posts={relatedPosts} />
</article>
);
}
Pros:
- Lightning fast (served from CDN)
- Excellent SEO
- Cheap hosting
- Great for content sites
Cons:
- Build times increase with more pages
- Not suitable for user-specific content
- Changes require rebuilds
Performance: 100-300ms load time.
Incremental Static Regeneration (ISR)
What it is: Static pages that can be updated in the background without full rebuilds.
When I use it:
- News websites
- Product catalogs
- Content that updates regularly
- High-traffic sites with dynamic data
Real Implementation
// News article page
export async function getStaticProps({ params }) {
const article = await fetchArticle(params.slug);
const comments = await fetchComments(params.slug);
return {
props: {
article,
comments,
lastUpdated: new Date().toISOString()
},
// Regenerate every 60 seconds if there's traffic
revalidate: 60
};
}
export async function getStaticPaths() {
// Only pre-generate recent articles
const recentArticles = await getRecentArticles(50);
return {
paths: recentArticles.map(article => ({
params: { slug: article.slug }
})),
// Generate older articles on first request
fallback: 'blocking'
};
}
function NewsArticle({ article, comments, lastUpdated }) {
return (
<div>
<article>
<h1>{article.title}</h1>
<time>{article.publishedAt}</time>
<div dangerouslySetInnerHTML={{ __html: article.content }} />
</article>
<CommentsSection comments={comments} />
<small>Last updated: {lastUpdated}</small>
</div>
);
}
Pros:
- Fast like SSG
- Fresh content
- Scales well
- Good SEO
Cons:
- Complex caching behavior
- Potential for stale content
- Harder to debug
Performance: 150-400ms (cached), 800ms-1.5s (regenerating).
My Decision Framework
Here’s how I choose:
1. Does it need SEO?
- Yes: SSR or SSG/ISR
- No: CSR is fine
2. How often does content change?
- Never/Rarely: SSG
- Hourly/Daily: ISR
- Real-time: SSR or CSR
3. Is it user-specific?
- Yes: CSR or SSR
- No: SSG or ISR
4. What’s the traffic like?
- High: SSG/ISR (CDN benefits)
- Low: SSR is fine
Real Project Examples
E-commerce Site
- Homepage: SSG (rarely changes)
- Product pages: ISR (inventory updates)
- User dashboard: CSR (personal data)
- Checkout: SSR (SEO + security)
News Website
- Article pages: ISR (content updates)
- Homepage: ISR (new articles)
- Admin panel: CSR (internal tool)
- Author pages: SSG (static bios)
SaaS Application
- Landing page: SSG (marketing)
- Documentation: SSG (static content)
- App dashboard: CSR (interactive)
- Pricing page: SSG (rarely changes)
Hybrid Implementation
// Different strategies in one Next.js app
// pages/index.js - SSG for landing
export async function getStaticProps() {
return { props: { hero: await getHeroContent() } };
}
// pages/blog/[slug].js - ISR for articles
export async function getStaticProps() {
return {
props: { post: await getPost() },
revalidate: 3600 // 1 hour
};
}
// pages/dashboard/[...slug].js - CSR for app
function Dashboard() {
// Client-side data fetching
const { data } = useSWR('/api/dashboard', fetcher);
return <DashboardUI data={data} />;
}
// pages/product/[id].js - SSR for SEO
export async function getServerSideProps() {
return { props: { product: await getProduct() } };
}
Performance Monitoring
I track these metrics for each strategy:
// Core Web Vitals tracking
function trackPerformance(strategy) {
// Largest Contentful Paint
new PerformanceObserver((list) => {
const lcp = list.getEntries()[0];
analytics.track('LCP', {
value: lcp.startTime,
strategy: strategy
});
}).observe({ entryTypes: ['largest-contentful-paint'] });
// First Input Delay
new PerformanceObserver((list) => {
const fid = list.getEntries()[0];
analytics.track('FID', {
value: fid.processingStart - fid.startTime,
strategy: strategy
});
}).observe({ entryTypes: ['first-input'] });
}
Common Mistakes I’ve Made
- Using SSR for everything - Killed server performance
- Pure CSR for content sites - SEO disaster
- ISR without proper cache headers - Confused users with stale content
- SSG for user-specific data - Security nightmare
Tools I Use
- Next.js: Best framework for mixing strategies
- Vercel Analytics: Performance monitoring
- Lighthouse CI: Automated performance testing
- React Query: Client-side data management
The Bottom Line
There’s no perfect rendering strategy. I usually end up with:
- Marketing pages: SSG
- Content pages: ISR
- User dashboards: CSR
- Critical SEO pages: SSR
Start with what you know, measure performance, and optimize based on real user data. Don’t over-engineer from day one.