Skip to main content

🎭 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:

  1. What is Virtual DOM? - Core principle of React rendering
  2. Web Performance Optimization (document in progress) - Methods to improve performance
  3. 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!