🎭 CSR vs SSR vs SSG
📖 Definition
There are three main ways to generate and display web pages:
- CSR (Client-Side Rendering): Creating pages with JavaScript in the browser
- SSR (Server-Side Rendering): Completing HTML on the server and sending it
- SSG (Static Site Generation): Pre-generating HTML at build time
Each method has its pros and cons, and you should choose according to your project characteristics.
🎯 Understanding Through Analogies
Restaurant Analogy
Comparing the three rendering methods to restaurants:
CSR (Client-Side Rendering)
= Self-Cooking Restaurant 🍳
Customer (Browser):
1. Enter restaurant (receive empty HTML)
2. Receive ingredients (download JavaScript)
3. Cook yourself (rendering)
4. Eat finished food
Pros: Low kitchen (server) burden, free cooking
Cons: Cooking time required, difficult for beginners
---
SSR (Server-Side Rendering)
= Regular Restaurant 🍽️
Kitchen (Server):
1. Take order (request)
2. Cook (generate HTML)
3. Serve finished food (send HTML)
Customer (Browser):
Can eat immediately!
Pros: Fast initial loading, search engine friendly
Cons: Heavy kitchen burden, cooking needed every time
---
SSG (Static Site Generation)
= Lunchbox Chain Store 🍱
Build Time:
1. Prepare all lunchboxes in advance
2. Store in refrigerator (CDN)
Customer (Browser):
1. Order
2. Receive packaged lunchbox immediately
3. Eat
Pros: Super fast delivery, low cost
Cons: Difficult to change menu, limited freshness
Architecture Analogy
CSR = Flat-pack Furniture (IKEA) 🪑
- Parts and manual delivery
- Assemble at home yourself
- Cheap shipping
- Assembly time required
SSR = Custom Furniture 🛋️
- Made to order
- Finished product delivery
- Ready to use immediately
- High cost, time consuming
SSG = Ready-made Furniture 🪟
- Pre-manufactured
- Stored in warehouse
- Immediate delivery
- Cheap and fast
- No customization
⚙️ How It Works
1. CSR (Client-Side Rendering)
// CSR Flow
// Step 1: Server sends minimal HTML
// index.html
<!DOCTYPE html>
<html>
<head>
<title>CSR App</title>
</head>
<body>
<div id="root"></div> <!-- Empty! -->
<script src="/bundle.js"></script>
</body>
</html>
// Step 2: Browser downloads JavaScript
// GET /bundle.js (2MB)
// Step 3: Execute JavaScript
ReactDOM.render(<App />, document.getElementById('root'));
// Step 4: Fetch data via API call
fetch('/api/posts')
.then(res => res.json())
.then(data => setPost(data));
// Step 5: Screen rendering complete
// Timeline:
// 0ms: HTML arrives (blank screen)
// 100ms: JavaScript download starts
// 1000ms: JavaScript parsing and execution
// 1500ms: API call
// 2000ms: Data arrives
// 2100ms: Screen display complete ✅
// What user sees:
// 0-2100ms: White screen or loading spinner
// 2100ms~: Complete page
2. SSR (Server-Side Rendering)
// SSR Flow
// Step 1: Client request
// GET /posts/123
// Step 2: Fetch data on server
// server.js (Node.js + Express)
app.get('/posts/:id', async (req, res) => {
// Database query
const post = await db.posts.findById(req.params.id);
// Convert React component to HTML string
const html = ReactDOMServer.renderToString(
<PostPage post={post} />
);
// Send completed HTML
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>${post.title}</title>
</head>
<body>
<div id="root">${html}</div>
<script>
window.__INITIAL_DATA__ = ${JSON.stringify(post)};
</script>
<script src="/bundle.js"></script>
</body>
</html>
`);
});
// Step 3: Browser receives HTML
// Can display screen immediately! (HTML is complete)
// Step 4: Hydration
// Connect event listeners when JavaScript loads
ReactDOM.hydrate(<PostPage />, document.getElementById('root'));
// Timeline:
// 0ms: Request
// 50ms: Query data on server
// 100ms: Generate HTML
// 200ms: HTML arrives
// 250ms: Display screen ✅ (Fast!)
// 1000ms: Load JavaScript
// 1100ms: Hydration complete (interactive)
// What user sees:
// 0-250ms: Loading
// 250-1100ms: Visible but not clickable
// 1100ms~: Fully functional
3. SSG (Static Site Generation)
// SSG Flow
// Build Time (before deployment)
// next build
// Step 1: Pre-generate all pages
// pages/posts/[id].js
export async function getStaticPaths() {
// List of pages to generate
const posts = await db.posts.findAll();
return {
paths: posts.map(post => ({
params: { id: post.id.toString() }
})),
fallback: false
};
}
export async function getStaticProps({ params }) {
// Data for each page
const post = await db.posts.findById(params.id);
return {
props: { post }
};
}
// Step 2: Generate HTML files
// .next/server/pages/posts/1.html
// .next/server/pages/posts/2.html
// .next/server/pages/posts/3.html
// ...
// Step 3: Deploy to CDN
// Vercel, Netlify, CloudFront, etc.
// Runtime (when user requests)
// GET /posts/123
// CDN returns HTML immediately (super fast!)
// Timeline:
// 0ms: Request
// 10ms: CDN returns HTML ✅ (Very fast!)
// 500ms: Load JavaScript
// 600ms: Hydration complete
// What user sees:
// 0-10ms: Loading
// 10-600ms: Visible but not clickable
// 600ms~: Fully functional
💡 Real Examples
CSR Example (Create React App)
// src/App.js
import React, { useState, useEffect } from 'react';
function App() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// API call in browser
fetch('https://api.example.com/posts')
.then(res => res.json())
.then(data => {
setPosts(data);
setLoading(false);
})
.catch(error => {
console.error('Error:', error);
setLoading(false);
});
}, []);
if (loading) {
return <div>Loading...</div>;
}
return (
<div className="app">
<h1>Blog Posts</h1>
<ul>
{posts.map(post => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</li>
))}
</ul>
</div>
);
}
export default App;
// Pros:
// 1. No server load
// 2. Fast page transitions (SPA)
// 3. Rich interactions
// Cons:
// 1. Slow initial loading
// 2. SEO difficulties (empty HTML)
// 3. JavaScript required
// Suitable for:
// - Admin dashboards
// - Apps used after login
// - Services that don't need SEO
SSR Example (Next.js)
// pages/posts/[id].js
import { useRouter } from 'next/router';
function Post({ post }) {
const router = useRouter();
// Loading state (fallback)
if (router.isFallback) {
return <div>Loading...</div>;
}
return (
<article>
<h1>{post.title}</h1>
<p>{post.author}</p>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
// Runs on server (every request)
export async function getServerSideProps({ params }) {
// Database or API call
const res = await fetch(`https://api.example.com/posts/${params.id}`);
const post = await res.json();
// 404 handling
if (!post) {
return {
notFound: true
};
}
// Pass as props
return {
props: {
post
}
};
}
export default Post;
// HTML generated on server:
// <!DOCTYPE html>
// <html>
// <head>
// <title>Next.js</title>
// </head>
// <body>
// <div id="__next">
// <article>
// <h1>This is a Title</h1>
// <p>Author: John Doe</p>
// <div>
// <p>Content...</p>
// </div>
// </article>
// </div>
// <script src="/_next/static/chunks/main.js"></script>
// </body>
// </html>
// Pros:
// 1. Fast initial loading
// 2. SEO optimized
// 3. Real-time data
// Cons:
// 1. High server costs
// 2. Variable response time
// 3. Difficult caching
// Suitable for:
// - News sites
// - Social media feeds
// - Real-time updates needed
SSG Example (Next.js)
// pages/blog/[slug].js
function BlogPost({ post }) {
return (
<article>
<h1>{post.title}</h1>
<time>{post.date}</time>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
// Runs at build time: which pages to generate
export async function getStaticPaths() {
// Fetch all posts
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();
// List of paths to generate
const paths = posts.map(post => ({
params: { slug: post.slug }
}));
return {
paths,
fallback: 'blocking' // New pages handled with SSR
};
}
// Runs at build time: data for each page
export async function getStaticProps({ params }) {
const res = await fetch(`https://api.example.com/posts/${params.slug}`);
const post = await res.json();
return {
props: {
post
},
revalidate: 60 // ISR: regenerate every 60 seconds
};
}
export default BlogPost;
// Build result:
// .next/server/pages/blog/first-post.html
// .next/server/pages/blog/second-post.html
// .next/server/pages/blog/third-post.html
// Pros:
// 1. Super fast loading
// 2. No server load
// 3. Perfect SEO
// 4. Cheap hosting
// Cons:
// 1. Long build time
// 2. No real-time data
// 3. Problems with many pages
// Suitable for:
// - Blogs
// - Documentation sites
// - Marketing pages
// - Portfolios
ISR (Incremental Static Regeneration)
// Combining the benefits of SSG + SSR!
// pages/products/[id].js
function Product({ product }) {
return (
<div>
<h1>{product.name}</h1>
<p>Price: ${product.price}</p>
<p>Stock: {product.stock} units</p>
</div>
);
}
export async function getStaticPaths() {
// Pre-generate only 100 popular products
const popularProducts = await getPopularProducts(100);
return {
paths: popularProducts.map(p => ({
params: { id: p.id.toString() }
})),
fallback: 'blocking' // Generate others on request
};
}
export async function getStaticProps({ params }) {
const product = await getProduct(params.id);
return {
props: { product },
revalidate: 10 // Revalidate every 10 seconds
};
}
export default Product;
// How it works:
// 1. At build: Generate HTML for 100 popular products
// 2. First request: Generate other products with SSR then cache
// 3. After 10 seconds: Regenerate in background
// 4. Always maintain latest data!
// Pros:
// - Fast build time
// - Fast response speed
// - Provide latest data
// - Unlimited pages possible
Hybrid Example
// Mixing multiple methods in Next.js app
// 1. Homepage - SSG (static)
// pages/index.js
export async function getStaticProps() {
return {
props: { hero: '...' },
revalidate: 3600 // Every hour
};
}
// 2. Blog list - ISR
// pages/blog/index.js
export async function getStaticProps() {
const posts = await getPosts();
return {
props: { posts },
revalidate: 60 // Every minute
};
}
// 3. Blog post - SSG
// pages/blog/[slug].js
export async function getStaticPaths() {
const posts = await getPosts();
return {
paths: posts.map(p => ({ params: { slug: p.slug }})),
fallback: 'blocking'
};
}
export async function getStaticProps({ params }) {
const post = await getPost(params.slug);
return {
props: { post },
revalidate: 3600
};
}
// 4. User dashboard - CSR
// pages/dashboard.js
function Dashboard() {
const [data, setData] = useState(null);
useEffect(() => {
// Runs only on client
fetch('/api/user/dashboard')
.then(res => res.json())
.then(setData);
}, []);
return <div>{/* Dashboard UI */}</div>;
}
// 5. Search results - SSR
// pages/search.js
export async function getServerSideProps({ query }) {
const results = await search(query.q);
return {
props: { results, query: query.q }
};
}
// Apply optimal rendering method to each page!
SEO Comparison
<!-- CSR: What search engines see (before JavaScript execution) -->
<!DOCTYPE html>
<html>
<head>
<title>React App</title>
</head>
<body>
<div id="root"></div>
<!-- No content! -->
<script src="/static/js/main.js"></script>
</body>
</html>
<!-- Search engines recognize as empty page ❌ -->
<!-- SSR/SSG: What search engines see -->
<!DOCTYPE html>
<html>
<head>
<title>Best React Tutorial</title>
<meta name="description" content="The easiest way to learn React">
<meta property="og:title" content="Best React Tutorial">
<meta property="og:image" content="https://example.com/og.jpg">
</head>
<body>
<div id="__next">
<article>
<h1>Best React Tutorial</h1>
<p>We introduce the easiest way to learn React...</p>
<section>
<h2>1. What is React?</h2>
<p>React is for building user interfaces...</p>
</section>
</article>
</div>
</body>
</html>
<!-- Has complete content! ✅ -->
<!-- Google, Naver index well -->
<!-- Social media previews work -->
Performance Comparison
// Web performance metrics
// CSR (Create React App)
const csrMetrics = {
FCP: '2.5s', // First Contentful Paint
LCP: '3.5s', // Largest Contentful Paint
TTI: '4.0s', // Time to Interactive
TBT: '500ms', // Total Blocking Time
CLS: '0.1', // Cumulative Layout Shift
// User experience:
// 0-2.5s: White screen
// 2.5-4s: Loading spinner
// 4s: Fully usable
};
// SSR (Next.js)
const ssrMetrics = {
FCP: '0.8s', // ⬆️ Fast!
LCP: '1.2s', // ⬆️ Fast!
TTI: '2.5s', // ⬆️ Fast!
TBT: '300ms', // ⬆️ Low!
CLS: '0.05', // ⬆️ Low!
// User experience:
// 0-0.8s: Loading
// 0.8-2.5s: Visible but not clickable
// 2.5s: Fully usable
};
// SSG (Next.js)
const ssgMetrics = {
FCP: '0.3s', // ⬆️⬆️ Very fast!
LCP: '0.5s', // ⬆️⬆️ Very fast!
TTI: '1.5s', // ⬆️⬆️ Very fast!
TBT: '100ms', // ⬆️⬆️ Very low!
CLS: '0.02', // ⬆️⬆️ Very low!
// User experience:
// 0-0.3s: Loading
// 0.3-1.5s: Visible but not clickable
// 1.5s: Fully usable
};
// Conclusion:
// SSG > SSR > CSR (initial loading speed)
// CSR = SSR = SSG (subsequent navigation)
🤔 Frequently Asked Questions
Q1. Which rendering method should I choose?
A: Choose based on your project characteristics:
// Selection Guide
// 1. Choose CSR:
const csrUseCases = {
conditions: [
'SEO not important',
'App used after login',
'Lots of real-time interaction',
'Save server costs'
],
examples: [
'Admin dashboard',
'Chat application',
'Games',
'Design tools (Figma, etc.)',
'Music player',
'Todo management app'
],
framework: 'Create React App, Vite'
};
// 2. Choose SSR:
const ssrUseCases = {
conditions: [
'SEO important',
'Real-time data needed',
'User-specific content',
'Fast initial loading'
],
examples: [
'News sites',
'Social media',
'E-commerce product pages',
'Search result pages',
'Real-time stock info',
'Weather app'
],
framework: 'Next.js, Nuxt.js, SvelteKit'
};
// 3. Choose SSG:
const ssgUseCases = {
conditions: [
'SEO important',
'Content rarely changes',
'Maximum performance needed',
'Cheap hosting'
],
examples: [
'Blog',
'Documentation site',
'Marketing landing page',
'Portfolio',
'Company info',
'Product catalog'
],
framework: 'Next.js, Gatsby, Astro'
};
// 4. Hybrid (recommended!):
const hybridUseCases = {
strategy: 'Use different methods per page',
example: {
'/': 'SSG', // Homepage
'/about': 'SSG', // About
'/blog': 'ISR', // Blog list
'/blog/[slug]': 'SSG', // Blog post
'/products': 'ISR', // Product list
'/products/[id]': 'ISR', // Product detail
'/search': 'SSR', // Search results
'/dashboard': 'CSR', // Dashboard
'/profile': 'CSR' // Profile
},
framework: 'Next.js (best choice)'
};
// Decision tree:
function chooseRenderingMethod(project) {
// Need SEO?
if (!project.needsSEO) {
return 'CSR';
}
// Real-time data?
if (project.needsRealtime) {
return 'SSR';
}
// Content changes often?
if (project.contentChangesOften) {
return 'ISR'; // SSG + regeneration
}
// Static content
return 'SSG';
}
Q2. What rendering method is Next.js?
A: Next.js supports all methods:
// Next.js - Hybrid framework
// 1. SSG (default)
// pages/about.js
function About() {
return <div>About Us</div>;
}
export default About;
// Without getStaticProps, automatically SSG
// 2. SSG with Data
// pages/blog/[slug].js
export async function getStaticPaths() {
return {
paths: [{ params: { slug: 'hello' }}],
fallback: false
};
}
export async function getStaticProps({ params }) {
const post = await getPost(params.slug);
return { props: { post }};
}
// 3. ISR (Incremental Static Regeneration)
export async function getStaticProps() {
return {
props: { data: '...' },
revalidate: 60 // Regenerate every 60 seconds
};
}
// 4. SSR
// pages/news.js
export async function getServerSideProps() {
const news = await getLatestNews();
return { props: { news }};
}
// 5. CSR
// pages/dashboard.js
function Dashboard() {
const { data } = useSWR('/api/user', fetcher);
return <div>{data}</div>;
}
// 6. API Routes (serverless functions)
// pages/api/hello.js
export default function handler(req, res) {
res.status(200).json({ message: 'Hello' });
}
// Next.js advantages:
// - File-based routing
// - Automatic code splitting
// - Image optimization
// - TypeScript support
// - Fast Refresh
// - Vercel deployment optimization
Q3. What is Hydration?
A: It's the process of making static HTML interactive in SSR/SSG:
// Hydration process
// Step 1: Generate HTML on server
// server.js
const html = ReactDOMServer.renderToString(<App />);
// Generated HTML:
<div id="root">
<button>Click (0)</button>
</div>
// Step 2: Send to browser
// User can see immediately!
// But button doesn't work (no event listeners)
// Step 3: Load and execute JavaScript
// client.js
ReactDOM.hydrate(<App />, document.getElementById('root'));
// During hydration:
// 1. React analyzes DOM tree
// 2. Compare with Virtual DOM
// 3. Connect event listeners
// 4. Initialize state
// Step 4: Hydration complete
// Now button works! Interactive!
// Hydration issues:
// Mismatch warning
const MismatchComponent = () => {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
return (
<div>
{/* ❌ Server: "Server", Client: "Client" */}
{typeof window === 'undefined' ? 'Server' : 'Client'}
{/* ✅ Render only after hydration */}
{mounted && <ClientOnlyComponent />}
</div>
);
};
// Hydration optimization:
// 1. Reduce initial HTML size
// 2. Reduce JavaScript bundle size
// 3. Inline critical CSS
// 4. Hydrate only necessary parts (Progressive Hydration)
Q4. Can CSR's SEO issues be solved?
A: There are several methods but not perfect:
// CSR SEO improvement methods
// 1. Prerendering (generate HTML at build time)
// Using react-snap
// package.json
{
"scripts": {
"postbuild": "react-snap"
}
}
// Serve pre-generated HTML when crawler bots visit
// Real users operate with CSR
// 2. Server-Side Rendering Service
// Use services like Prerender.io, Rendertron
// nginx configuration
location / {
if ($http_user_agent ~* "googlebot|bingbot|yandex") {
proxy_pass http://prerender-service;
}
}
// 3. Google Search Console
// - JavaScript rendering test
// - Check robots.txt
// - Submit sitemap.xml
// 4. Dynamic meta tag update
import { Helmet } from 'react-helmet';
function BlogPost({ post }) {
return (
<>
<Helmet>
<title>{post.title}</title>
<meta name="description" content={post.excerpt} />
<meta property="og:title" content={post.title} />
<meta property="og:image" content={post.image} />
</Helmet>
<article>{/* ... */}</article>
</>
);
}
// 5. Structured Data (JSON-LD)
<script type="application/ld+json">
{`
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "${post.title}",
"author": "${post.author}",
"datePublished": "${post.date}"
}
`}
</script>
// Limitations:
// - Perfect SEO is impossible
// - Complex and hard to maintain
// - SSR/SSG is better
// Conclusion: Use SSR/SSG if SEO is important!
Q5. How to measure rendering performance?
A: Can be measured with several tools:
// 1. Lighthouse (Chrome DevTools)
// Chrome DevTools > Lighthouse tab
// - Performance
// - Accessibility
// - Best Practices
// - SEO
// Key metrics:
const webVitals = {
FCP: 'First Contentful Paint', // First content display
LCP: 'Largest Contentful Paint', // Largest content display
FID: 'First Input Delay', // First input delay
TTI: 'Time to Interactive', // Until interactive
TBT: 'Total Blocking Time', // Total blocking time
CLS: 'Cumulative Layout Shift' // Cumulative layout shift
};
// 2. web-vitals library
import { getCLS, getFID, getFCP, getLCP, getTTI } from 'web-vitals';
function sendToAnalytics({ name, value, id }) {
console.log(name, value);
// Send to Google Analytics
gtag('event', name, {
event_category: 'Web Vitals',
value: Math.round(value),
event_label: id,
non_interaction: true
});
}
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getFCP(sendToAnalytics);
getLCP(sendToAnalytics);
getTTI(sendToAnalytics);
// 3. Next.js Analytics
// pages/_app.js
export function reportWebVitals(metric) {
console.log(metric);
// {
// name: 'FCP',
// value: 1234.5,
// id: 'v2-1234567890'
// }
}
// 4. Chrome DevTools Performance
// 1. DevTools > Performance tab
// 2. Start recording
// 3. Refresh page
// 4. Stop recording
// 5. Analyze:
// - Loading: HTML, CSS, JS download
// - Scripting: JavaScript execution
// - Rendering: Layout, paint
// - Painting: Draw pixels
// 5. Network tab
// - Resource loading time
// - File size
// - Waterfall chart
// - Parallel downloads
// 6. React DevTools Profiler
import { Profiler } from 'react';
function onRenderCallback(
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime
) {
console.log(`${id} (${phase}) took ${actualDuration}ms`);
}
<Profiler id="App" onRender={onRenderCallback}>
<App />
</Profiler>
// Target metrics:
const goodMetrics = {
FCP: '< 1.8s',
LCP: '< 2.5s',
FID: '< 100ms',
TTI: '< 3.8s',
TBT: '< 200ms',
CLS: '< 0.1'
};
🎓 Next Steps
Now that you understand rendering methods, try learning these next:
- What is Virtual DOM? - Core principle of React rendering
- Web Performance Optimization (document in progress) - Methods to improve performance
- SEO Basics (document in progress) - Search engine optimization
🎬 Conclusion
Rendering methods greatly affect web application performance and SEO:
- CSR: Rich interactions, SEO difficulties
- SSR: Fast initial loading, high server costs
- SSG: Best performance, suitable for static content
- ISR: SSG + automatic updates, best combination
Choose the rendering method that fits your project characteristics, and combine them in a hybrid approach if needed!