
SSR vs CSR: Where to Render the Screen
Server sends cooked meal (SSR) vs Server sends ingredients, browser cooks (CSR). SEO vs initial load trade-off. Why Next.js combines both.

Server sends cooked meal (SSR) vs Server sends ingredients, browser cooks (CSR). SEO vs initial load trade-off. Why Next.js combines both.
Why does my server crash? OS's desperate struggle to manage limited memory. War against Fragmentation.

Two ways to escape a maze. Spread out wide (BFS) or dig deep (DFS)? Who finds the shortest path?

A comprehensive deep dive into client-side storage. From Cookies to IndexedDB and the Cache API. We explore security best practices for JWT storage (XSS vs CSRF), performance implications of synchronous APIs, and how to build offline-first applications using Service Workers.

Fast by name. Partitioning around a Pivot. Why is it the standard library choice despite O(N²) worst case?

I built a personal blog with React and published posts for about a month, but it never showed up in Google search. When I checked Google Search Console, I got a bizarre message: "Crawled but found no content." But there was clearly content!
After some research, I found articles saying "SPAs built with Create React App have poor SEO." They said CSR was the problem. What's SSR then? People suggested using Next.js to solve it, but I couldn't understand why.
What does "rendering pages on the server" even mean? Doesn't React run in the browser? This curiosity kicked off my SSR/CSR learning journey.
When I first started learning about SSR/CSR, several things puzzled me.
I understood "render" means "draw the screen," but I couldn't grasp what "server renders the screen" meant. Doesn't the server just fetch data from the database and send JSON? Doesn't the browser draw the screen?
I thought React was a library that manipulates the DOM using document.getElementById('root'). But servers don't have DOM, so how can you run React on the server?
Aren't Google bots just browsers? If they execute JavaScript like browsers, why can't they crawl CSR apps?
The word means "adding water" literally, but are we pouring water on code? It seemed like a metaphor for "injecting" JavaScript into HTML, but I didn't know exactly what process it described.
The core issue was understanding how servers and browsers divide their responsibilities.My first moment of understanding came from a cooking analogy I read on someone's blog.
CSR (Client Side Rendering): Restaurant (server) gives only ingredients (JSON data) → Guest (browser) cooks at home (renders)
- Pros: Less restaurant burden, guests can cook as they like
- Cons: Takes time from receiving ingredients to finishing cooking, can't smell the food from the street (no SEO)
SSR (Server Side Rendering): Restaurant (server) gives a cooked meal (complete HTML)
- Pros: Can eat immediately (fast initial load), can smell the food (SEO works)
- Cons: Heavy restaurant burden (server load), kitchen must cook for every order
This analogy clicked because mapping "rendering = cooking" made clear who does what and where. CSR means guests cook with ingredients; SSR means the kitchen cooks and delivers. This difference affects performance and SEO.
CSR is essentially "the browser creates the DOM with JavaScript." Apps built with Create React App are typical examples.
1. Browser: "GET / HTTP/1.1 (give me index.html)"
2. Server: "Here you go" (almost empty HTML file)
<!-- HTML the server sends -->
<html>
<head>
<title>My App</title>
</head>
<body>
<div id="root"></div> <!-- Empty! -->
<script src="/static/js/bundle.js"></script> <!-- 2.5MB -->
</body>
</html>
3. Browser: Starts downloading bundle.js (2.5MB, takes 2 seconds on 3G)
4. Browser: Executes bundle.js (parsing + compilation 0.5 seconds)
5. React runs ReactDOM.render(<App />, document.getElementById('root'))
6. Creates Virtual DOM → Manipulates Real DOM → Renders to screen
7. User: Finally sees the screen (3-4 seconds total elapsed)
Key point: HTML is just a shell; JavaScript creates all the UI.
Understanding how React SPAs (the quintessential CSR approach) work helps you understand CSR itself.
// src/App.js
function App() {
const [count, setCount] = useState(0);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
// When executed in browser
ReactDOM.render(<App />, document.getElementById('root'));
When this code runs:
<App /> component → Transforms JSX into React.createElement() callsInitially <div id="root"></div> is empty, then React fills it when executed. That's the essence of CSR.
// React Router example
import { BrowserRouter, Route, Link } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
</nav>
<Route path="/" exact component={Home} />
<Route path="/about" component={About} />
</BrowserRouter>
);
}
When you click the /about link:
This is what "Single Page" in SPA means. There's actually only one page; JavaScript swaps the screen content.
CSR's problem was large initial bundle size. Code Splitting emerged to solve this.
// Traditional import (everything in bundle)
import HeavyComponent from './HeavyComponent';
// Lazy import (load only when needed)
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
}
This approach:
But the problem of first page being empty HTML still remains.
The server just needs to serve static files (HTML, JS, CSS, images) with nginx or Apache. If you host on a CDN, the server barely does anything.
# nginx.conf
server {
listen 80;
root /var/www/html;
location / {
try_files $uri /index.html; # All requests to index.html
}
}
Since users' browsers handle rendering, even with 10,000 concurrent users, the server just transmits files.
// Navigate from /posts to /posts/123
<Link to="/posts/123">
<h2>{post.title}</h2>
</Link>
// On click:
// - No server request
// - No HTML re-fetch
// - Just swap components with JavaScript
// - Screen transition within 50ms
This is SPA's biggest appeal: app-like smooth UX.
Frontend: React App (S3 + CloudFront)
Backend: REST API (AWS Lambda)
You can completely separate frontend as static hosting and backend as API server. Each can be deployed and scaled independently.
Receive HTML (1KB) → Screen stays empty
↓
Download JS (2.5MB, 3 seconds on 3G)
↓
Parse/compile JS (1 second on mobile CPU)
↓
React executes → Creates DOM
↓
Finally see screen (5 seconds total)
Users see a white screen for 5 seconds. This is called the "White Screen of Death."
HTML that Google bot crawls:
<!-- HTML Google bot receives -->
<html>
<head>
<title>My Blog</title>
</head>
<body>
<div id="root"></div> <!-- Empty! -->
<script src="/static/js/bundle.js"></script>
</body>
</html>
Google bot thinks "this page has no content" and leaves. While Google bot does execute JavaScript nowadays:
Result: Lower search rankings.
<!-- HTML Facebook/Twitter scrapes -->
<html>
<head>
<meta property="og:title" content="???" />
<meta property="og:image" content="???" />
</head>
<body>
<div id="root"></div>
</body>
</html>
OG (Open Graph) tags are empty, so sharing links on KakaoTalk/Slack shows no thumbnail.
SSR is the concept of "server executes React to create HTML."
1. Browser: "GET / HTTP/1.1"
2. Server: Runs React in Node.js
- ReactDOMServer.renderToString(<App />)
- Result: HTML string
3. Server: "Here's the complete HTML"
<!-- HTML the server sends -->
<html>
<head>
<title>My Blog</title>
</head>
<body>
<div id="root">
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
<main>
<h1>Welcome to My Blog</h1>
<p>This content is server-rendered!</p>
</main>
</div>
<script src="/static/js/bundle.js"></script>
</body>
</html>
4. Browser: Receives HTML and immediately renders → Screen visible! (0.3s)
5. Downloads bundle.js (background)
6. Hydration: Attaches event listeners
7. Becomes interactive
Key point: HTML already has full content. Browser just needs to render it immediately.
I thought React was browser-only, so how can it run on the server?
// server.js (Node.js)
import express from 'express';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import App from './App';
const app = express();
app.get('*', (req, res) => {
// Convert React component to HTML string
const html = ReactDOMServer.renderToString(<App />);
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>SSR App</title>
</head>
<body>
<div id="root">${html}</div>
<script src="/bundle.js"></script>
</body>
</html>
`);
});
app.listen(3000);
What renderToString does:
<App /> componentThis works because React was designed "platform-independent." React Core doesn't need to know about DOM; ReactDOM handles DOM. On servers, ReactDOMServer just creates HTML strings.
The traditional renderToString problem was having to build the entire HTML before sending a response.
// Before: renderToString (blocking)
const html = ReactDOMServer.renderToString(<App />);
// Wait until all components render (e.g., 2 seconds)
// Only then send response
res.send(html); // Send all at once after 2 seconds
React 18's renderToPipeableStream supports streaming.
// After: renderToPipeableStream (streaming)
import { renderToPipeableStream } from 'react-dom/server';
app.get('*', (req, res) => {
const { pipe } = renderToPipeableStream(<App />, {
onShellReady() {
// Start sending as soon as shell is ready
res.setHeader('Content-Type', 'text/html');
pipe(res);
}
});
});
// Stream HTML in chunks
// User sees first screen in 0.3s,
// Rest fills in as it loads
This significantly improved SSR performance.
Receive HTML (50KB) → Immediately render screen (0.3s)
↓
Download JS (background)
↓
Hydration → Interactive
Users see "something meaningful" in 0.3 seconds. Huge difference compared to CSR's 5 seconds.
TTV (Time To View): When users see meaningful content. SSR is overwhelmingly faster.
<!-- HTML Google bot receives (SSR) -->
<html>
<head>
<title>Clean Code Principles Summary | Codemapo</title>
<meta name="description" content="Variable naming, function writing..." />
<meta property="og:image" content="/images/clean-code.png" />
</head>
<body>
<h1>Clean Code Principles Summary</h1>
<p>Clean Code means readable code...</p>
<p>First principle is meaningful variable names...</p>
</body>
</html>
With full content:
SSR is well-known to be beneficial for SEO. There are many reported cases of significant improvements in search visibility after switching from CSR to SSR.
CSR requires browser CPU to execute JavaScript, which is slow on low-end phones. SSR has the server pre-build HTML, so browsers just render (rendering is well-optimized in browsers).
// If 100 concurrent requests come in
// Node.js server runs React 100 times
// CPU 100% → Response delay
// Solution: Caching
import NodeCache from 'node-cache';
const cache = new NodeCache({ stdTTL: 60 });
app.get('/posts/:id', (req, res) => {
const cached = cache.get(req.params.id);
if (cached) {
return res.send(cached);
}
const html = renderToString(<Post id={req.params.id} />);
cache.set(req.params.id, html);
res.send(html);
});
Caching helps somewhat, but server load is still higher than CSR.
CSR:
Browser request → nginx reads and sends index.html
TTFB: 50ms (just file reading)
SSR:
Browser request → Node.js runs React → Generates HTML → Sends
TTFB: 300ms (React execution time)
TTFB is slower, but TTV is faster. Trade-off.
// This code errors on server
function MyComponent() {
const width = window.innerWidth; // ReferenceError: window is not defined
return <div>Width: {width}</div>;
}
// Solution: Run only in browser
function MyComponent() {
const [width, setWidth] = useState(0);
useEffect(() => {
setWidth(window.innerWidth); // Runs only in browser
}, []);
return <div>Width: {width}</div>;
}
Servers don't have window, document, localStorage, etc., so you must be careful.
Hydration is a core SSR concept that was initially hard to understand.
Hydration: The process of "breathing life" into static HTML created by the server. Specifically, attaching event listeners and starting state management.
<!-- HTML sent by server (static) -->
<button>Click me</button>
<!-- This button doesn't do anything when clicked yet -->
// Client-side Hydration
import { hydrateRoot } from 'react-dom/client';
function App() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>Click me</button>;
}
// Run Hydration
hydrateRoot(document.getElementById('root'), <App />);
// Now button click increments count!
What hydrateRoot does:
onClick, etc.)A common SSR error is "Hydration mismatch."
// Server rendering
function ServerTime() {
const now = new Date().toISOString();
return <div>Server time: {now}</div>;
}
// Server: "2025-06-21T10:30:00Z"
// Client Hydration: "2025-06-21T10:30:05Z" (5 second difference)
// → Hydration mismatch! Warning triggered
This happens when HTML generated by server and client differ.
Solution:
function ServerTime() {
const [now, setNow] = useState(null);
useEffect(() => {
setNow(new Date().toISOString()); // Runs only on client
}, []);
if (!now) {
return <div>Loading...</div>; // Server and client first render match
}
return <div>Client time: {now}</div>;
}
React 18's "Selective Hydration" significantly improved performance.
// Before: Hydrate entire tree at once (blocking)
hydrateRoot(document, <App />);
// Nothing interactive until all components hydrated
// After: Prioritize with Suspense
function App() {
return (
<div>
<Header /> {/* Hydrate immediately */}
<Suspense fallback={<Spinner />}>
<Comments /> {/* Hydrate later */}
</Suspense>
</div>
);
}
A "smart" approach that prioritizes hydrating parts users interact with first.
Between SSR and CSR, there's another approach. SSG (Static Site Generation): Pre-building HTML at build time.
# Build time
npm run build
→ Next.js pre-renders all pages
→ /out/index.html (complete HTML)
→ /out/posts/1.html (complete HTML)
→ /out/posts/2.html (complete HTML)
# Runtime
User request → nginx serves static HTML
→ Zero server burden, maximum speed
// pages/posts/[id].js
export async function getStaticProps({ params }) {
// Runs once at build time
const post = await fetchPost(params.id);
return {
props: { post },
revalidate: 60, // ISR: Regenerate every 60 seconds
};
}
export async function getStaticPaths() {
// Specify which pages to pre-build
const posts = await fetchAllPosts();
return {
paths: posts.map(p => ({ params: { id: p.id } })),
fallback: 'blocking', // Generate missing pages on request
};
}
function Post({ post }) {
return <article>{post.content}</article>;
}
ISR solves SSG's disadvantage (no updates after build).
export async function getStaticProps() {
const data = await fetch('https://api.example.com/data');
return {
props: { data },
revalidate: 10, // Regenerate in background every 10 seconds
};
}
How it works:
Captures both "static page speed + dynamic data freshness."
React Server Components, released in 2023, advanced SSR one step further.
React Server Components: Components that run only on server and don't send JavaScript bundles to client.
// app/page.js (Server Component - default)
async function HomePage() {
// Runs only on server (direct database access possible)
const posts = await db.query('SELECT * FROM posts');
return (
<div>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
// This component's JavaScript is NOT sent to client!
// Bundle size: 0KB
// app/PostList.server.js (Server Component)
async function PostList() {
const posts = await db.posts.findMany(); // Direct DB access
return (
<div>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
// Not included in client bundle
// app/LikeButton.client.js (Client Component)
'use client'; // Explicitly declare client component
export function LikeButton() {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked(!liked)}>
{liked ? '❤️' : '🤍'}
</button>
);
}
// Included in client bundle (needs to be interactive)
Key point: Non-interactive parts as Server Components, only necessary parts as Client Components.
// Before: Traditional SSR
// - Server generates HTML
// - Client downloads all component code (2MB)
// - Hydration
// After: RSC
// - Server generates HTML
// - Client downloads only Client Components (200KB)
// - Hydrate only Client Components
Bundle size reduced by 90%! That's RSC's core value.
Next.js provides two routers.
pages/
index.js → / (choose SSR/SSG/CSR)
about.js → /about
posts/[id].js → /posts/:id
// pages/posts/[id].js
export async function getServerSideProps({ params }) {
// SSR: Runs on every request
const post = await fetchPost(params.id);
return { props: { post } };
}
// Or
export async function getStaticProps({ params }) {
// SSG: Runs at build time
const post = await fetchPost(params.id);
return { props: { post } };
}
function PostPage({ post }) {
return <article>{post.content}</article>;
}
app/
page.js → / (default Server Component)
about/page.js → /about
posts/[id]/page.js → /posts/:id
// app/posts/[id]/page.js (Server Component)
async function PostPage({ params }) {
// Async components possible! (runs only on server)
const post = await fetchPost(params.id);
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
<LikeButton /> {/* Client Component */}
</article>
);
}
// app/LikeButton.js (Client Component)
'use client';
export function LikeButton() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>Like {count}</button>;
}
Difference:
getServerSidePropsApp Router is more intuitive, and smaller bundle size thanks to RSC.
Let me summarize key web performance metrics and SSR/CSR's impact.
Google's 3 critical metrics:
"Time when largest content renders on screen"
CSR: 3.5s (after JS loads and renders)
SSR: 0.8s (already in HTML)
SSG: 0.3s (cached HTML)
Goal: Within 2.5 seconds
SSR/SSG overwhelmingly better.
"Delay from user input to response"
CSR: 300ms (after Hydration completes)
SSR: 200ms (faster Hydration)
SSG: 150ms
Goal: Within 100ms
"How much layout shifts during page load"
CSR: 0.15 (content suddenly appears after JS loads → layout shift)
SSR: 0.05 (already in HTML, less shift)
Goal: Below 0.1
"Time when first content renders on screen"
CSR: 2.5s (after JS loads)
SSR: 0.5s (HTML immediately)
"Time when page becomes fully interactive"
CSR: 3.8s (JS load + execution)
SSR: 2.5s (Hydration)
SSG: 2.0s
Comparing actual deployment methods and costs for each rendering approach.
# Build
npm run build # → Creates static files in build/ folder
# Upload to S3
aws s3 sync build/ s3://my-bucket --delete
# Deploy with CloudFront CDN
# → Served quickly from worldwide edges
Cost:
Pros:
Cons:
# Deploy Next.js project to Vercel
vercel deploy
# Vercel automatically:
# - Runs Node.js server
# - Distributes to edge network
# - Auto-scales
Cost:
Pros:
Cons:
# Build Next.js SSG
next build && next export
# Deploy to Cloudflare Pages
wrangler pages publish out/
Cost:
Pros:
Cons:
# Dockerfile
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
RUN npm run build
CMD ["npm", "start"]
# Build Docker image and deploy to ECS
docker build -t my-app .
aws ecr push my-app
aws ecs update-service --service my-app
Cost:
Pros:
Cons:
// pages/posts/[id].js
// Method 1: SSR (server render on every request)
export async function getServerSideProps({ params, req, res }) {
// Runs on every request
console.log('Running SSR...');
// Optional caching headers
res.setHeader(
'Cache-Control',
'public, s-maxage=10, stale-while-revalidate=59'
);
const post = await fetch(`https://api.example.com/posts/${params.id}`)
.then(res => res.json());
if (!post) {
return { notFound: true };
}
return {
props: {
post,
renderedAt: new Date().toISOString(),
},
};
}
// Method 2: SSG (once at build time)
export async function getStaticProps({ params }) {
// Runs once at build time
console.log('Running SSG...');
const post = await fetch(`https://api.example.com/posts/${params.id}`)
.then(res => res.json());
return {
props: { post },
revalidate: 60, // ISR: Regenerate every 60 seconds
};
}
export async function getStaticPaths() {
// Specify which paths to pre-generate
const posts = await fetch('https://api.example.com/posts')
.then(res => res.json());
return {
paths: posts.map(p => ({ params: { id: String(p.id) } })),
fallback: 'blocking', // Generate missing paths on request
};
}
// Component
function PostPage({ post, renderedAt }) {
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
{renderedAt && <small>Rendered at: {renderedAt}</small>}
</article>
);
}
export default PostPage;
// app/posts/[id]/page.js (Server Component)
import { Suspense } from 'react';
import { LikeButton } from './LikeButton';
import { Comments } from './Comments';
// Server components can be async!
async function PostPage({ params }) {
// Direct database access (runs only on server)
const post = await db.posts.findUnique({
where: { id: params.id },
});
if (!post) {
notFound(); // Go to 404 page
}
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
{/* Client Component: Interactive */}
<LikeButton postId={post.id} />
{/* Suspense for loading */}
<Suspense fallback={<div>Loading comments...</div>}>
<Comments postId={post.id} />
</Suspense>
</article>
);
}
export default PostPage;
// app/posts/[id]/LikeButton.js (Client Component)
'use client'; // Explicitly client component
import { useState } from 'react';
export function LikeButton({ postId }) {
const [liked, setLiked] = useState(false);
const [count, setCount] = useState(0);
const handleLike = async () => {
setLiked(!liked);
// API call
const res = await fetch(`/api/posts/${postId}/like`, {
method: 'POST',
});
const data = await res.json();
setCount(data.likeCount);
};
return (
<button onClick={handleLike}>
{liked ? '❤️' : '🤍'} {count}
</button>
);
}
// app/posts/[id]/Comments.js (Server Component)
async function Comments({ postId }) {
// Data fetching on server
const comments = await db.comments.findMany({
where: { postId },
});
return (
<div>
<h3>Comments</h3>
{comments.map(comment => (
<div key={comment.id}>
<strong>{comment.author}</strong>
<p>{comment.content}</p>
</div>
))}
</div>
);
}
// Next.js project structure
pages/
index.js // SSG (main page, rarely changes)
blog/[slug].js // SSG + ISR (blog posts)
dashboard/index.js // CSR (requires login, no SEO needed)
products/[id].js // SSR (real-time stock info)
// pages/index.js (SSG)
export async function getStaticProps() {
return {
props: {
posts: await fetchLatestPosts(),
},
revalidate: 3600, // Regenerate every hour
};
}
// pages/blog/[slug].js (SSG + ISR)
export async function getStaticProps({ params }) {
return {
props: {
post: await fetchPost(params.slug),
},
revalidate: 60, // Regenerate every minute
};
}
// pages/dashboard/index.js (CSR)
function Dashboard() {
const [data, setData] = useState(null);
useEffect(() => {
// Fetch data only on client
fetch('/api/user/dashboard')
.then(res => res.json())
.then(setData);
}, []);
if (!data) return <div>Loading...</div>;
return <div>{data.username}'s Dashboard</div>;
}
// No getServerSideProps = CSR
// pages/products/[id].js (SSR)
export async function getServerSideProps({ params }) {
// Latest stock info on every request
return {
props: {
product: await fetchProduct(params.id),
},
};
}
Let me summarize my experience migrating my blog from CSR (Create React App) → SSR (Next.js).
Tech stack:
- Create React App
- React Router
- Markdown parsing (client-side)
- S3 + CloudFront deployment
Problems:
1. Not showing in Google search (critical)
2. Initial load 3.5s (5s on mobile)
3. No thumbnail when sharing links
4. Lighthouse SEO score: 45
Tech stack:
- Next.js 14 (App Router)
- Server Components
- MDX (server-side parsing)
- Vercel deployment
Improvements:
1. Google search traffic 0 → 100 daily visitors (within 2 weeks)
2. Initial load 0.8s (4x faster)
3. OG images working properly
4. Lighthouse SEO score: 100
5. All Core Web Vitals passing
// Blog posts: SSG + ISR (fastest)
export const revalidate = 3600;
// Search page: CSR (no SEO needed)
'use client';
// Real-time comments: Server Component (SEO needed)
async function Comments() { }
// Before
<img src="/images/cover.jpg" /> // 2MB original
// After
import Image from 'next/image';
<Image
src="/images/cover.jpg"
width={800}
height={400}
alt="Cover"
loading="lazy"
/>
// Next.js auto converts to WebP, resizes, lazy loads
// Before: All libraries in main bundle
import { format } from 'date-fns'; // Import entire library
// After: Only what's needed
import format from 'date-fns/format'; // Tree-shaking
// Result: Bundle size 2.5MB → 800KB
The conclusion I reached was "it depends on the situation."
Examples:
- Admin dashboard
- Internal tools
- Pages behind login
Reason: If Google exposure isn't needed anyway, CSR is simplest and cheapest.
Examples:
- Chat apps
- Real-time collaboration tools (Figma, Notion)
- Games
Reason: SSR only speeds up initial load; subsequent interactions happen on client anyway. Starting with CSR is simpler.
For 100K monthly visitors:
- CSR (S3 + CloudFront): $10
- SSR (Vercel Pro): $20
- SSR (self-hosted): $100
Examples:
- Blogs
- News sites
- E-commerce (product pages)
- Corporate websites
Reason: If Google search traffic is critical, SSR is essential.
<!-- OG tags must be server-generated -->
<meta property="og:title" content="Actual title" />
<meta property="og:image" content="Actual image" />
Need thumbnails when sharing on KakaoTalk/Slack? Use SSR.
Examples:
- News sites (so readers don't leave immediately)
- Landing pages (directly affects conversion)
- Mobile web (low-end devices)
Examples:
- Documentation sites
- Blogs (posts edited infrequently)
- Portfolios
- Marketing pages
Reason: Fastest, cheapest, most stable.
If no personalization needed, SSG is best.
export const revalidate = 60; // Regenerate every minute
// First user: Cached page (fast)
// User after 1 min: Background regeneration → Fresh data
Can capture both "static page speed + dynamic data freshness."
Let me summarize the core lessons I learned while writing this.
SSR isn't unconditionally better. Each has clear pros and cons.
CSR:
✅ Low server burden, simple deployment, cheap
❌ Weak SEO, slow initial load
SSR:
✅ Strong SEO, fast initial load
❌ Heavy server burden, complex deployment, expensive
SSG:
✅ Fastest, cheap, stable
❌ Limited dynamic data
Next.js is popular because "you can use both."
// Different per page
pages/
index.js → SSG (main page)
blog/[slug].js → SSG + ISR (blog)
dashboard.js → CSR (dashboard)
products/[id].js → SSR (real-time stock)
Mixing approaches based on use case in one project was the answer.
"Fast/slow" is subjective. Lighthouse measurement is objective.
Metrics:
- LCP: Within 2.5s
- FID: Within 100ms
- CLS: Below 0.1
- Lighthouse Performance: Above 90
When starting a project:
Q1. Need Google search exposure?
→ YES: Consider SSR/SSG
→ NO: CSR is enough
Q2. Is social media sharing important?
→ YES: SSR/SSG
→ NO: CSR
Q3. Is initial load very important?
→ YES: SSR/SSG
→ NO: CSR is fine
Answering these questions makes the choice clear.
I understood React Server Components as a game changer.
Before: All components in client bundle
After: Only interactive ones in client bundle
Result: 90% bundle size reduction possible
Major frameworks like Next.js App Router and Remix adopted RSC. Studying in this direction felt right.
Ultimately "there's no right answer" was the right answer.