SSR vs CSR
Server-Side vs Client-Side Rendering
Explanation
Rendering Strategies
How your app renders content affects performance, SEO, and user experience. Understanding the trade-offs helps you choose the right approach.
Key Concepts
- CSR: JavaScript renders content in the browser
- SSR: Server generates HTML for each request
- SSG: HTML generated at build time
- ISR: Static with on-demand regeneration
- Hydration: Making static HTML interactive
Comparison
| Aspect | CSR | SSR | SSG | |--------|-----|-----|-----| | Initial Load | Slower | Faster | Fastest | | SEO | Challenging | Good | Good | | Server Load | Low | High | Low | | Dynamic Data | Easy | Moderate | Needs ISR | | TTFB | Fast | Slower | Fastest |
Demonstration
Example 1: Client-Side Rendering (React)
// CSR - React with useEffect
import { useState, useEffect } from 'react';
function ProductList() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchProducts();
}, []);
async function fetchProducts() {
try {
const res = await fetch('/api/products');
const data = await res.json();
setProducts(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
// What the browser receives:
// <div id="root"></div>
// <script src="/bundle.js"></script>
// Then JavaScript:
// 1. Downloads bundle
// 2. Executes React
// 3. Fetches data
// 4. Renders content
Example 2: Server-Side Rendering (Next.js)
// pages/products/index.js - SSR with getServerSideProps
export default function ProductsPage({ products }) {
return (
<div>
<h1>Products</h1>
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
export async function getServerSideProps(context) {
// Runs on every request ON THE SERVER
const res = await fetch(`${process.env.API_URL}/products`);
const products = await res.json();
// Handle errors
if (!res.ok) {
return {
notFound: true,
// or redirect: { destination: '/error', permanent: false }
};
}
return {
props: { products }
};
}
// What the browser receives:
// Complete HTML with data already rendered
// <div>
// <h1>Products</h1>
// <div class="product">Product 1</div>
// <div class="product">Product 2</div>
// </div>
Example 3: Static Site Generation (Next.js)
// pages/posts/[slug].js - SSG with getStaticProps
export default function PostPage({ post }) {
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
// Generate paths at build time
export async function getStaticPaths() {
const res = await fetch(`${process.env.API_URL}/posts`);
const posts = await res.json();
const paths = posts.map(post => ({
params: { slug: post.slug }
}));
return {
paths,
fallback: 'blocking' // or true or false
};
}
// Generate page content at build time
export async function getStaticProps({ params }) {
const res = await fetch(`${process.env.API_URL}/posts/${params.slug}`);
const post = await res.json();
if (!post) {
return { notFound: true };
}
return {
props: { post },
revalidate: 3600 // ISR: Regenerate every hour
};
}
Example 4: Incremental Static Regeneration
// pages/products/[id].js - ISR
export default function ProductPage({ product, lastUpdated }) {
return (
<div>
<h1>{product.name}</h1>
<p>${product.price}</p>
<p>Stock: {product.stock}</p>
<small>Last updated: {lastUpdated}</small>
</div>
);
}
export async function getStaticPaths() {
// Only pre-generate top products
const res = await fetch(`${process.env.API_URL}/products/top`);
const products = await res.json();
return {
paths: products.map(p => ({ params: { id: p.id.toString() } })),
fallback: 'blocking' // Generate other pages on-demand
};
}
export async function getStaticProps({ params }) {
const res = await fetch(`${process.env.API_URL}/products/${params.id}`);
const product = await res.json();
return {
props: {
product,
lastUpdated: new Date().toISOString()
},
revalidate: 60 // Regenerate every minute if requested
};
}
// How ISR works:
// 1. User requests /products/123
// 2. If cached version exists and is fresh, serve it
// 3. If stale, serve cached version AND trigger regeneration
// 4. Next request gets the new version
Example 5: Hybrid Approach
// pages/dashboard.js - Hybrid: SSR shell + CSR data
import { useState, useEffect } from 'react';
import { useSession } from 'next-auth/react';
// Shell rendered on server, data fetched on client
export default function Dashboard({ initialStats }) {
const { data: session } = useSession();
const [stats, setStats] = useState(initialStats);
const [activities, setActivities] = useState([]);
// Fetch real-time data on client
useEffect(() => {
if (session) {
fetchRealTimeData();
const interval = setInterval(fetchRealTimeData, 30000);
return () => clearInterval(interval);
}
}, [session]);
async function fetchRealTimeData() {
const [statsRes, activitiesRes] = await Promise.all([
fetch('/api/stats'),
fetch('/api/activities')
]);
setStats(await statsRes.json());
setActivities(await activitiesRes.json());
}
return (
<div>
<h1>Dashboard</h1>
{/* Static content - SEO friendly */}
<StaticWelcome />
{/* Dynamic content - client-side */}
<StatsDisplay stats={stats} />
<ActivityFeed activities={activities} />
</div>
);
}
export async function getServerSideProps(context) {
// Get initial data for faster first paint
const res = await fetch(`${process.env.API_URL}/stats/summary`);
const initialStats = await res.json();
return {
props: { initialStats }
};
}
Example 6: Choosing the Right Strategy
// Decision tree for rendering strategy
const renderingGuide = {
// Public, SEO-important, rarely changes
"Marketing pages": "SSG",
"Blog posts": "SSG or ISR",
"Documentation": "SSG",
// Public, SEO-important, changes frequently
"Product listings": "ISR",
"News articles": "ISR or SSR",
"Search results": "SSR",
// Private, user-specific
"Dashboard": "SSR + CSR",
"User profile": "SSR",
"Settings": "CSR",
// Real-time data
"Chat": "CSR + WebSocket",
"Live scores": "CSR + polling",
"Notifications": "CSR"
};
// Next.js 13+ App Router patterns
// app/products/page.js - Server Component (default)
export default async function ProductsPage() {
// This runs on the server
const products = await fetch(`${process.env.API_URL}/products`);
return <ProductList products={products} />;
}
// app/products/[id]/page.js - With caching
export default async function ProductPage({ params }) {
// Cache for 1 hour
const product = await fetch(`${process.env.API_URL}/products/${params.id}`, {
next: { revalidate: 3600 }
});
return <ProductDetail product={product} />;
}
// Client Component for interactivity
'use client';
export function AddToCartButton({ productId }) {
const [loading, setLoading] = useState(false);
async function handleClick() {
setLoading(true);
await addToCart(productId);
setLoading(false);
}
return (
<button onClick={handleClick} disabled={loading}>
{loading ? 'Adding...' : 'Add to Cart'}
</button>
);
}
Key Takeaways:
- SSG for static, SEO-important content
- SSR for dynamic, personalized pages
- CSR for highly interactive features
- ISR bridges static and dynamic
- Hybrid approaches often work best
Imitation
Challenge 1: Convert CSR to SSR
Task: Convert this CSR component to use SSR.
// Before: CSR
function UserProfile() {
const [user, setUser] = useState(null);
useEffect(() => {
fetch('/api/user').then(r => r.json()).then(setUser);
}, []);
return user ? <Profile user={user} /> : <Loading />;
}
Solution
// After: SSR
export default function UserProfile({ user }) {
return <Profile user={user} />;
}
export async function getServerSideProps(context) {
// Get auth from cookies
const { req } = context;
const token = req.cookies.token;
if (!token) {
return {
redirect: {
destination: '/login',
permanent: false
}
};
}
const res = await fetch(`${process.env.API_URL}/user`, {
headers: { Authorization: `Bearer ${token}` }
});
if (!res.ok) {
return { notFound: true };
}
const user = await res.json();
return {
props: { user }
};
}
Challenge 2: Implement ISR for E-commerce
Task: Set up ISR for a product catalog with proper cache invalidation.
Solution
// pages/products/[slug].js
export default function ProductPage({ product }) {
return (
<div>
<h1>{product.name}</h1>
<p className={product.stock > 0 ? 'in-stock' : 'out-of-stock'}>
{product.stock > 0 ? `${product.stock} in stock` : 'Out of stock'}
</p>
</div>
);
}
export async function getStaticPaths() {
const products = await fetch(`${process.env.API_URL}/products`).then(r => r.json());
return {
paths: products.slice(0, 100).map(p => ({
params: { slug: p.slug }
})),
fallback: 'blocking'
};
}
export async function getStaticProps({ params }) {
const product = await fetch(`${process.env.API_URL}/products/${params.slug}`)
.then(r => r.json());
if (!product) {
return { notFound: true };
}
return {
props: { product },
revalidate: 60 // Revalidate every minute
};
}
// pages/api/revalidate.js - On-demand revalidation
export default async function handler(req, res) {
const { secret, slug } = req.query;
if (secret !== process.env.REVALIDATE_SECRET) {
return res.status(401).json({ message: 'Invalid token' });
}
try {
await res.revalidate(`/products/${slug}`);
return res.json({ revalidated: true });
} catch (err) {
return res.status(500).json({ message: 'Error revalidating' });
}
}
// Call from your CMS webhook when product updates
Practice
Exercise 1: Build a Blog with SSG
Difficulty: Intermediate
Create a blog that:
- Generates pages at build time
- Uses ISR for comments
- Has client-side search
Exercise 2: E-commerce Product Pages
Difficulty: Advanced
Implement:
- ISR for product pages
- Real-time stock updates (CSR)
- SEO-optimized metadata
- On-demand revalidation
Summary
What you learned:
- CSR vs SSR vs SSG trade-offs
- When to use each rendering strategy
- Implementing ISR for dynamic static sites
- Hybrid approaches combining strategies
- Next.js rendering patterns
Next Steps:
- Read: Performance Optimization
- Practice: Migrate an SPA to SSR
- Explore: Edge rendering and streaming
