🎭 CSR vs SSR vs SSG
📖 定義
網頁生成和顯示的方式主要分為三種:
- CSR (Client-Side Rendering): 使用 JavaScript 在瀏覽器中生成頁面
- SSR (Server-Side Rendering): 在伺服器上完成 HTML 並發送
- SSG (Static Site Generation): 在建置時預先生成 HTML
每種方式都有其優缺點,需要根據專案特性選擇。
🎯 以比喻理解
餐廳比喻
將三種渲染方式比作餐廳:
CSR (Client-Side Rendering)
= 自助料理餐廳 🍳
顧客(瀏覽器):
1. 進入餐廳(接收空白 HTML)
2. 取得食材(下載 JavaScript)
3. 自行烹飪(渲染)
4. 享用完成的食物
優點:廚房(伺服器)負擔輕,自由烹飪
缺點:需要烹飪時間,新手較難
---
SSR (Server-Side Rendering)
= 一般餐廳 🍽️
廚房(伺服器):
1. 接受訂單(請求)
2. 烹飪(生成 HTML)
3. 提供完成的食物(傳送 HTML)
顧客(瀏覽器):
可立即食用!
優點:初始載入快速,搜尋引擎友好
缺點:廚房負擔大,每次都需要烹飪
---
SSG (Static Site Generation)
= 便當連鎖店 🍱
建置時間:
1. 預先準備所有便當
2. 存放在冰箱(CDN)
顧客(瀏覽器):
1. 下訂單
2. 立即取得包裝好的便當
3. 食用
優點:超高速配送,成本低
缺點:難以更改菜單,新鮮度有限
家具製作比喻
CSR = 自行組裝家具(宜家)🪑
- 零件和說明書運送
- 在家自行組裝
- 運費便宜
- 需要組裝時間
SSR = 客製化家具 🛋️
- 訂製
- 運送成品
- 可立即使用
- 成本高,耗時
SSG = 現成家具 🪟
- 預先製作
- 存放在倉庫
- 立即發貨
- 便宜且快速
- 無法客製
⚙️ 運作原理
1. CSR (Client-Side Rendering)
// CSR 流程
// 第一階段:伺服器傳送最小 HTML
// index.html
<!DOCTYPE html>
<html>
<head>
<title>CSR App</title>
</head>
<body>
<div id="root"></div> <!-- 空白! -->
<script src="/bundle.js"></script>
</body>
</html>
// 第二階段:瀏覽器下載 JavaScript
// GET /bundle.js (2MB)
// 第三階段:執行 JavaScript
ReactDOM.render(<App />, document.getElementById('root'));
// 第四階段:呼叫 API 取得資料
fetch('/api/posts')
.then(res => res.json())
.then(data => setPost(data));
// 第五階段:完成畫面渲染
// 時間軸:
// 0ms: HTML 到達(空白畫面)
// 100ms: 開始下載 JavaScript
// 1000ms: 解析及執行 JavaScript
// 1500ms: 呼叫 API
// 2000ms: 資料到達
// 2100ms: 完成畫面顯示 ✅
// 使用者看到的:
// 0-2100ms: 白色畫面或載入提示
// 2100ms~: 完成頁面
2. SSR (Server-Side Rendering)
// SSR 流程
// 第一階段:客戶端請求
// GET /posts/123
// 第二階段:伺服器取得資料
// server.js (Node.js + Express)
app.get('/posts/:id', async (req, res) => {
// 查詢資料庫
const post = await db.posts.findById(req.params.id);
// 將 React 元件轉換為 HTML 字串
const html = ReactDOMServer.renderToString(
<PostPage post={post} />
);
// 傳送完成的 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>
`);
});
// 第三階段:瀏覽器接收 HTML
// 可立即顯示畫面!(HTML 已完成)
// 第四階段:Hydration
// JavaScript 載入後連接事件監聽器
ReactDOM.hydrate(<PostPage />, document.getElementById('root'));
// 時間軸:
// 0ms: 請求
// 50ms: 伺服器查詢資料
// 100ms: 生成 HTML
// 200ms: HTML 到達
// 250ms: 顯示畫面 ✅(快速!)
// 1000ms: 載入 JavaScript
// 1100ms: Hydration 完成(互動)
// 使用者看到的:
// 0-250ms: 載入
// 250-1100ms: 可見但無法點擊
// 1100ms~: 完全可用
3. SSG (Static Site Generation)
// SSG 流程
// 建置時間(部署前)
// next build
// 第一階段:預先生成所有頁面
// pages/posts/[id].js
export async function getStaticPaths() {
// 要生成的頁面列表
const posts = await db.posts.findAll();
return {
paths: posts.map(post => ({
params: { id: post.id.toString() }
})),
fallback: false
};
}
export async function getStaticProps({ params }) {
// 每個頁面的資料
const post = await db.posts.findById(params.id);
return {
props: { post }
};
}
// 第二階段 :生成 HTML 檔案
// .next/server/pages/posts/1.html
// .next/server/pages/posts/2.html
// .next/server/pages/posts/3.html
// ...
// 第三階段:部署到 CDN
// Vercel, Netlify, CloudFront 等
// 執行時間(使用者請求時)
// GET /posts/123
// CDN 立即返回 HTML(超高速!)
// 時間軸:
// 0ms: 請求
// 10ms: CDN 返回 HTML ✅(非常快!)
// 500ms: 載入 JavaScript
// 600ms: Hydration 完成
// 使用者看到的:
// 0-10ms: 載入
// 10-600ms: 可見但無法點擊
// 600ms~: 完全可用
💡 實際範例
CSR 範例 (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
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>載入中...</div>;
}
return (
<div className="app">
<h1>部落格文章</h1>
<ul>
{posts.map(post => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</li>
))}
</ul>
</div>
);
}
export default App;
// 優點:
// 1. 伺服器負擔輕
// 2. 頁面切換快速(SPA)
// 3. 豐富互動性
// 缺點:
// 1. 初始載入慢
// 2. SEO 困難(空白 HTML)
// 3. 必須使用 JavaScript
// 適合的情況:
// - 管理員儀表板
// - 登入後使用的應用
// - 不需要 SEO 的服務
SSR 範例 (Next.js)
// pages/posts/[id].js
import { useRouter } from 'next/router';
function Post({ post }) {
const router = useRouter();
// 載入狀態(fallback)
if (router.isFallback) {
return <div>載入中...</div>;
}
return (
<article>
<h1>{post.title}</h1>
<p>{post.author}</p>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
// 在伺服器端執行(每次請求)
export async function getServerSideProps({ params }) {
// 呼叫資料庫或 API
const res = await fetch(`https://api.example.com/posts/${params.id}`);
const post = await res.json();
// 處理 404
if (!post) {
return {
notFound: true
};
}
// 傳遞 props
return {
props: {
post
}
};
}
export default Post;
// 伺服器生成的 HTML:
// <!DOCTYPE html>
// <html>
// <head>
// <title>Next.js</title>
// </head>
// <body>
// <div id="__next">
// <article>
// <h1>標題</h1>
// <p>作者:王小明</p>
// <div>
// <p>內文...</p>
// </div>
// </article>
// </div>
// <script src="/_next/static/chunks/main.js"></script>
// </body>
// </html>
// 優點:
// 1. 初始載入快速
// 2. SEO 優化
// 3. 即時資料
// 缺點:
// 1. 伺服器成本高
// 2. 回應時間波動
// 3. 快取困難
// 適合的情況:
// - 新聞網站
// - 社交媒體動態
// - 需要即時更新的服務
### SSG 範例 (Next.js)
```jsx
// pages/blog/[slug].js
function BlogPost({ post }) {
return (
<article>
<h1>{post.title}</h1>
<time>{post.date}</time>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
// 建置時執行:決定要生成哪些頁面
export async function getStaticPaths() {
// 取得所有文章列表
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();
// 要生成的路徑列表
const paths = posts.map(post => ({
params: { slug: post.slug }
}));
return {
paths,
fallback: 'blocking' // 新頁面使用 SSR 處理
};
}
// 建置時執行:每個頁面的資料
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:每 60 秒重新生成
};
}
export default BlogPost;
// 建置結果:
// .next/server/pages/blog/first-post.html
// .next/server/pages/blog/second-post.html
// .next/server/pages/blog/third-post.html
// 優點:
// 1. 超快載入
// 2. 無伺服器負載
// 3. SEO 完美
// 4. 主機成本低
// 缺點:
// 1. 建置時間長
// 2. 無即時資料
// 3. 頁面過多會有問題
// 適合的場景:
// - 部落格
// - 文件網站
// - 行銷頁面
// - 作品集
ISR (增量靜態重新生成)
// 結合 SSG + SSR 的優點!
// pages/products/[id].js
function Product({ product }) {
return (
<div>
<h1>{product.name}</h1>
<p>價格:{product.price}元</p>
<p>庫存:{product.stock}個</p>
</div>
);
}
export async function getStaticPaths() {
// 只預先生成最受歡迎的 100 個商品
const popularProducts = await getPopularProducts(100);
return {
paths: popularProducts.map(p => ({
params: { id: p.id.toString() }
})),
fallback: 'blocking' // 其餘商品在請求時生成
};
}
export async function getStaticProps({ params }) {
const product = await getProduct(params.id);
return {
props: { product },
revalidate: 10 // 每 10 秒重新驗證
};
}
export default Product;
// 運作方式:
// 1. 建置時:生成最受歡迎的 100 個商品 HTML
// 2. 首次請求:其餘商品使用 SSR 生成並快取
// 3. 10 秒後:在背景重新生成
// 4. 始終保持最新資料!
// 優點:
// - 快速的建置時間
// - 快速的回應速度
// - 提供最新資料
// - 可以有無限的頁面
混合範例
// 在 Next.js 應用程式中混合多種方式
// 1. 首頁 - SSG(靜態)
// pages/index.js
export async function getStaticProps() {
return {
props: { hero: '...' },
revalidate: 3600 // 每小時
};
}
// 2. 部落格列表 - ISR
// pages/blog/index.js
export async function getStaticProps() {
const posts = await getPosts();
return {
props: { posts },
revalidate: 60 // 每分鐘
};
}
// 3. 部落格文章 - 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. 使用者儀表板 - CSR
// pages/dashboard.js
function Dashboard() {
const [data, setData] = useState(null);
useEffect(() => {
// 僅在用戶端執行
fetch('/api/user/dashboard')
.then(res => res.json())
.then(setData);
}, []);
return <div>{/* 儀表板 UI */}</div>;
}
// 5. 搜尋結果 - SSR
// pages/search.js
export async function getServerSideProps({ query }) {
const results = await search(query.q);
return {
props: { results, query: query.q }
};
}
// 每個頁面應用最佳的渲染方式!
SEO 比較
<!-- CSR:搜尋引擎看到的內容(執行 JavaScript 之前) -->
<!DOCTYPE html>
<html>
<head>
<title>React App</title>
</head>
<body>
<div id="root"></div>
<!-- 沒有內容! -->
<script src="/static/js/main.js"></script>
</body>
</html>
<!-- 搜尋引擎識別為空白頁面 ❌ -->
<!-- SSR/SSG:搜尋引擎看到的內容 -->
<!DOCTYPE html>
<html>
<head>
<title>最佳的 React 教學</title>
<meta name="description" content="學習 React 的最簡單方法">
<meta property="og:title" content="最佳的 React 教學">
<meta property="og:image" content="https://example.com/og.jpg">
</head>
<body>
<div id="__next">
<article>
<h1>最佳的 React 教學</h1>
<p>介紹學習 React 最簡單的方法...</p>
<section>
<h2>1. 什麼是 React?</h2>
<p>React 是用於建立使用者介面的...</p>
</section>
</article>
</div>
</body>
</html>
<!-- 有完整內容! ✅ -->
<!-- Google、Naver 可以很好地建立索引 -->
<!-- 社交媒體預覽可正常運作 -->
效能比較
// Web 效能指標
// 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
// 使用者體驗:
// 0-2.5s:白色畫面
// 2.5-4s:載入微調器
// 4s:完全可用
};
// SSR(Next.js)
const ssrMetrics = {
FCP: '0.8s', // ⬆️ 更快!
LCP: '1.2s', // ⬆️ 更快!
TTI: '2.5s', // ⬆️ 更快!
TBT: '300ms', // ⬆️ 更低!
CLS: '0.05', // ⬆️ 更低!
// 使用者體驗:
// 0-0.8s:載入
// 0.8-2.5s:可見但無法點擊
// 2.5s:完全可用
};
// SSG(Next.js)
const ssgMetrics = {
FCP: '0.3s', // ⬆️⬆️ 非常快!
LCP: '0.5s', // ⬆️⬆️ 非常快!
TTI: '1.5s', // ⬆️⬆️ 非常快!
TBT: '100ms', // ⬆️⬆️ 非常低!
CLS: '0.02', // ⬆️⬆️ 非常低!
// 使用者體驗:
// 0-0.3s:載入
// 0.3-1.5s:可見但無法點擊
// 1.5s:完全可用
};
// 結論:
// SSG > SSR > CSR(初始載入速度)
// CSR = SSR = SSG(後續導航)
常見問題
Q1. 我應該選擇哪種渲染方式?
A: 根據專案特性選擇:
// 選擇指南
// 1. 選擇 CSR:
const csrUseCases = {
conditions: [
'SEO 不重要',
'登入後使用的應用程式',
'大量即時互動',
'節省伺服器成本'
],
examples: [
'管理員儀表板',
'聊天應用程式',
'遊戲',
'設計工具(如 Figma)',
'音樂播放器',
'待辦事項管理應用程式'
],
framework: 'Create React App, Vite'
};
// 2. 選擇 SSR:
const ssrUseCases = {
conditions: [
'需要 SEO',
'需要即時資料',
'使用者專屬內容',
'快速初始載入'
],
examples: [
'新聞網站',
'社交媒體',
'e-commerce 商品頁面',
'搜尋結果頁面',
'即時股票資訊',
'天氣應用程式'
],
framework: 'Next.js, Nuxt.js, SvelteKit'
};
// 3. 選擇 SSG:
const ssgUseCases = {
conditions: [
'需要 SEO',
'內容變動不頻繁',
'需要最佳效能',
'低成本主機'
],
examples: [
'部落格',
'文件網站',
'行銷登陸頁面',
'作品集',
'公司介紹',
'產品目錄'
],
framework: 'Next.js, Gatsby, Astro'
};
// 4. 混合方式(推薦!):
const hybridUseCases = {
strategy: '不同頁面使用不同方式',
example: {
'/': 'SSG', // 首頁
'/about': 'SSG', // 關於
'/blog': 'ISR', // 部落格列表
'/blog/[slug]': 'SSG', // 部落格文章
'/products': 'ISR', // 商品列表
'/products/[id]': 'ISR', // 商品詳情
'/search': 'SSR', // 搜尋結果
'/dashboard': 'CSR', // 儀表板
'/profile': 'CSR' // 個人資料
},
framework: 'Next.js(最佳選擇)'
};
// 決策樹:
function chooseRenderingMethod(project) {
// 需要 SEO?
if (!project.needsSEO) {
return 'CSR';
}
// 需要即時資料?
if (project.needsRealtime) {
return 'SSR';
}
// 內容是否頻繁變更?
if (project.contentChangesOften) {
return 'ISR'; // SSG + 重新生成
}
// 靜態內容
return 'SSG';
}
SSG 範例 (Next.js)
// Next.js - 混合型框架
// 1. SSG (預設)
// pages/about.js
function About() {
return <div>公司介紹</div>;
}
export default About;
// 沒有 getStaticProps 時,自動使用 SSG
// 2. 帶資料的 SSG
// 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 (增量靜態重新生成)
export async function getStaticProps() {
return {
props: { data: '...' },
revalidate: 60 // 每 60 秒重新生成
};
}
// 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 (無伺服器函數)
// pages/api/hello.js
export default function handler(req, res) {
res.status(200).json({ message: 'Hello' });
}
// Next.js 的優點:
// - 基於檔案的路由
// - 自動程式碼分割
// - 圖片優化
// - TypeScript 支援
// - 快速重新整理
// - Vercel 部署最佳化
Q3. 什麼是 Hydration?
A: 在 SSR/SSG 中,將靜態 HTML 轉換為可互動的過程:
// Hydration 流程
// 第 1 階段:在伺服器上生成 HTML
// server.js
const html = ReactDOMServer.renderToString(<App />);
// 生成的 HTML:
<div id="root">
<button>點擊 (0)</button>
</div>
// 第 2 階段:傳送到瀏覽器
// 使用者可以立即看到!
// 但按鈕無法點擊(沒有事件監聽器)
// 第 3 階段:載入及執行 JavaScript
// client.js
ReactDOM.hydrate(<App />, document.getElementById('root'));
// Hydration 過程:
// 1. React 分析 DOM 樹
// 2. 與 Virtual DOM 比較
// 3. 連接事件監聽器
// 4. 初始化狀態
// 第 4 階段:Hydration 完成
// 現在按鈕可以點擊!變得可互動!
// Hydration 問題:
// 不一致警告
const MismatchComponent = () => {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
return (
<div>
{/* ❌ 伺服器:"伺服器",客戶端:"客戶端" */}
{typeof window === 'undefined' ? '伺服器' : '客戶端'}
{/* ✅ 只在 Hydration 後渲染 */}
{mounted && <ClientOnlyComponent />}
</div>
);
};
// Hydration 優化:
// 1. 減少初始 HTML 大小
// 2. 減小 JavaScript 套件大小
// 3. 內聯關鍵 CSS
// 4. 僅對需要的部分進行 Hydrate(漸進式 Hydration)
Q4. 可以解決 CSR 的 SEO 問題嗎?
A: 有幾種方法,但不是完美的解決方案:
// CSR SEO 改善方法
// 1. 預渲染(在建置時生成 HTML)
// 使用 react-snap
// package.json
{
"scripts": {
"postbuild": "react-snap"
}
}
// 當爬蟲機器人訪問時,提供預先生成的 HTML
// 實際用戶使用 CSR 模式
// 2. 伺服器端渲染服務
// 使用 Prerender.io、Rendertron 等服務
// nginx 設定
location / {
if ($http_user_agent ~* "googlebot|bingbot|yandex") {
proxy_pass http://prerender-service;
}
}
// 3. Google Search Console
// - 測試 JavaScript 渲染
// - 確認 robots.txt
// - 提交 sitemap.xml
// 4. 動態更新 Meta 標籤
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. 結構化資料(JSON-LD)
<script type="application/ld+json">
{`
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "${post.title}",
"author": "${post.author}",
"datePublished": "${post.date}"
}
`}
</script>
// 限制:
// - 完美的 SEO 是不可能的
// - 複雜且難以維護
// - SSR/SSG 更好
// 結論:如果 SEO 很重要,請使用 SSR/SSG!
Q5. 如何測量渲染效能?
A: 可以使用多種工具進行測量:
// 1. Lighthouse(Chrome DevTools)
// 在 Chrome DevTools 的 Lighthouse 標籤
// - 效能
// - 可訪問性
// - 最佳實踐
// - SEO
// 主要指標:
const webVitals = {
FCP: 'First Contentful Paint', // 首次內容繪製
LCP: 'Largest Contentful Paint', // 最大內容繪製
FID: 'First Input Delay', // 首次輸入延遲
TTI: 'Time to Interactive', // 可互動時間
TBT: 'Total Blocking Time', // 總阻塞時間
CLS: 'Cumulative Layout Shift' // 累積佈局偏移
};
// 2. web-vitals 函式庫
import { getCLS, getFID, getFCP, getLCP, getTTI } from 'web-vitals';
function sendToAnalytics({ name, value, id }) {
console.log(name, value);
// 發送到 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 標籤
// 2. 開始錄製
// 3. 重新載入頁面
// 4. 停止錄製
// 5. 分析:
// - 載入:HTML、CSS、JS 下載
// - 腳本:執行 JavaScript
// - 渲染:佈局、繪製
// - 繪製:繪製像素
// 5. Network 標籤
// - 資源載入時間
// - 檔案大小
// - 瀑布圖
// - 平行下載
// 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>
// 目標指標:
const goodMetrics = {
FCP: '< 1.8s',
LCP: '< 2.5s',
FID: '< 100ms',
TTI: '< 3.8s',
TBT: '< 200ms',
CLS: '< 0.1'
};
🎓 下一步
如果您已經理解渲染方式,請繼續學習以下內容:
- 什麼是 Virtual DOM? - React 渲染的核心原理
- 網頁性能最佳化(即將撰寫) - 效能改善方法
- SEO 基礎(即將撰寫) - 搜尋引擎最佳化
🎬 總結
渲染方式對網頁應用程式的效能和 SEO 有重大影響:
- CSR:豐富的互動,SEO 困難
- SSR:快速初始載入,伺服器成本高
- SSG:最佳效能,適合靜態內容
- ISR:SSG + 自動更新,最佳組合
請根據專案特性選擇適當的渲染方式,必要時可以混合使用!