客戶端渲染 vs 伺服器端渲染
客戶端渲染 vs 伺服器端渲染
核心差異
誰負責把 JSX 轉換成使用者看到的 HTML?
- SSR(Server-Side Rendering,伺服器端渲染) — 伺服器處理,瀏覽器收到現成的 HTML
- CSR(Client-Side Rendering,客戶端渲染) — 瀏覽器處理,伺服器只送出 JavaScript
流程比較
SSR(伺服器先渲染)
瀏覽器:GET /posts
↓
伺服器:
├─ 執行 src/app/posts/page.tsx
├─ 組合 layout.tsx + page.tsx
├─ 轉換成 HTML 字串
└─ 回傳給瀏覽器
↓
瀏覽器:收到完整的 HTML
├─ 立即顯示
└─ 再載入 JavaScript 處理互動
使用者立即看到內容。
CSR(客戶端稍後渲染)
瀏覽器:GET /index.html
↓
伺服器:回傳空的 HTML + JavaScript 程式碼
↓
瀏覽器:
├─ 下載 JavaScript(3-5 秒)
├─ 執行 React 程式碼
├─ 發送 fetch(/api/posts)
├─ 等待資料回傳
└─ 渲染頁面
↓
使用者終於看到內容(5-10 秒)
使用者先看到讀取畫面。
這個專案的範例
SSR 版本(Next.js 預設)
// src/app/posts/page.tsx
// 沒有 "use client" = 伺服器端渲染
export default function PostList() {
const { tableProps } = useTable();
return <Table {...tableProps} />;
}
發生的事:
1. 使用者造訪 /posts
2. 伺服器執行 page.tsx
3. useTable 在伺服器上呼叫 API,取得資料
4. 伺服器把 <Table data={[...]} /> 轉成 HTML
5. 瀏覽器收到:<html><body>...<table>...</table></body></html>
6. 頁面立即顯示
CSR 版本(加上 "use client")
"use client"; // ← 切換成客戶端渲染
import { useState, useEffect } from "react";
export default function PostList() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// 在瀏覽器抓資料
fetch("/api/posts")
.then(r => r.json())
.then(setPosts)
.finally(() => setLoading(false));
}, []);
if (loading) return <div>載入中...</div>;
return <Table dataSource={posts} />;
}
發生的事:
1. 使用者造訪 /posts
2. 伺服器回傳:<html><body><div>載入中...</div></body></html>
3. 瀏覽器顯示「載入中...」
4. JavaScript 載入完成,useEffect 執行
5. 瀏覽器發送 fetch(/api/posts)
6. 資料回傳,setPosts 更新狀態
7. 頁面重新渲染,表格顯示出來
使用者先看到讀取畫面,等 5-10 秒才看到表格。
比較表
| 指標 | SSR(伺服器端渲染) | CSR(客戶端渲染) |
|---|---|---|
| 使用者看到內容的時間 | 快(秒級) | 慢(5-10 秒) |
| 伺服器負擔 | 重(每次請求都要執行程式碼) | 輕(只送 HTML + JS) |
| SEO | ✅ 好(搜尋引擎讀得到 HTML) | ❌ 差(搜尋引擎看不到內容) |
| 互動延遲 | 快(JS 已載入) | 慢(要等 JS 下載完) |
| 頻寬消耗 | 較多(每次請求伺服器都要處理) | 較少(瀏覽器做大部分工作) |
| 可快取 | ✅ 可以(靜態 HTML) | ❌ 較難(動態 JS) |
| 離線可用 | ❌ 不行 | ✅ 可以 |
何時該用哪一種
使用 SSR
✅ 適合:
- 部落格、行銷網站、電商(需要 SEO)
- 需要快速的首屏顯示
- 大量靜態內容
- 需要在伺服器端直接存取資料庫
❌ 不適合:
- 即時、高度互動的應用程式
- 單頁應用程式(SPA)
使用 CSR
✅ 適合:
- 高互動應用程式(Discord、Figma、Notion)
- 不需要 SEO
- 前後端完全分離
- 需要離線功能
❌ 不適合:
- 首次載入時間很重要
- SEO 很重要
- 使用者網路較慢
Next.js 的策略
Next.js 預設使用 SSR,除非你加上 "use client":
預設 = SSR
↓
加上 "use client" = CSR
這一行就決定了一切:
"use client"; // ← 加上這行 = 客戶端渲染
這個專案目前的作法
src/app/layout.tsx ← SSR(伺服器渲染 HTML 外殼)
<html>
<Providers>
{children}
</Providers>
</html>
src/app/providers.tsx ← CSR(有 "use client")
<Refine dataProvider={...} />
src/app/posts/page.tsx ← CSR(有 "use client")
<useTable />
src/app/api/posts/route.ts ← 純伺服器端(API 路由)
pool.query(...)
混合模式:兩種方式的優點兼具。
- 外殼(layout)由伺服器渲染 → 快速
- 互動部分(頁面)由客戶端渲染 → 靈活
為什麼 Next.js 這樣設計
Next.js 讓你能針對應用程式的不同部分分別選擇:
- 伺服器元件(SSR)— 負責抓資料、直連資料庫、保管機密金鑰
- 客戶端元件(CSR)— 負責互動、hook、使用者輸入
這樣就不必再像過去那樣,整個應用程式只能二選一。
不只 SSR 與 CSR
SSR 與 CSR 其實只是其中一個維度而已。現代框架(包含 Next.js)還混合了幾種其他策略:
SSG(Static Site Generation,靜態網站產生)
- HTML 只在建置(build)時產生一次,不是每次請求都重新產生。
- 伺服器只需要送出靜態檔案(或走 CDN 邊緣快取)→ 回應速度最快。
- 內容只有在重新建置、重新部署時才會更新。
- 沒有動態資料抓取的頁面,或使用
generateStaticParams的頁面,預設就是這種模式。
ISR(Incremental Static Regeneration,增量式靜態再生)
- Next.js 特有,是 SSG 的進化版。
- 頁面維持靜態,但會在背景依
revalidate設定的秒數重新產生 — 內容更新不需要整站重新建置。
export const revalidate = 60; // 最多每 60 秒重新產生一次
Streaming SSR(串流式伺服器渲染)
- SSR 的變形:伺服器把 HTML 分段串流送出,而不是等整頁都算完才回應。
- 搭配 React 的
Suspense使用 — 比較慢的部分(例如某個要等資料庫查詢的元件)不會卡住其他先算好的部分。 - Next.js App Router 預設就是用這個方式。
RSC(React Server Components,伺服器元件)
- 這其實是完全不同的維度:重點是元件在哪裡執行,而不是HTML 何時產生。
- 伺服器元件(Server Component)的 JS 永遠不會送到瀏覽器(打包體積更小);客戶端元件(
"use client")才會送到瀏覽器並 hydrate。 - 這份文件裡的 SSR/CSR 其實是這個選擇帶來的副作用 —「use client」這行真正切換的是 RSC 與客戶端元件,而不是直接切換 SSR/CSR。
Islands / Resumability(島嶼架構 / 可續行架構)
- Astro、Qwik 採用的作法。
- 頁面大部分是靜態 HTML,只有零星的互動元件(「island」)才會載入 JS 並 hydrate。
- Qwik 的「resumability(可續行)」更進一步 — 完全跳過載入時重新執行 JS 的步驟,直接從伺服器結束的地方接續執行。
總結
| 渲染方式 | 誰做 | 何時 | 取捨 |
|---|---|---|---|
| SSR | 伺服器建構 HTML | 應用程式啟動時 | 伺服器要費力氣 |
| CSR | 瀏覽器建構 HTML | JS 載入完成後 | 使用者要等待 |
| 混合(Next.js) | 兩者配合,依元件個別選擇 | 依需求判斷 | 最佳使用體驗 |
Next.js 讓 SSR 變得很簡單,因為它是預設值。如果需要 CSR,加上 "use client" 就好。
延伸:何時該混合使用
推薦模式:
// 外殼 — SSR(資料量大或需要 metadata)
export default function RootLayout({ children }) {
const config = await fetchConfig(); // ← 伺服器端
return (
<html>
<body>{children}</body>
</html>
);
}
// 頁面 — CSR(需要互動)
"use client";
export default function Page() {
const [state, setState] = useState(); // ← 瀏覽器端
return <div>{state}</div>;
}
這樣使用者既能快速看到頁面(SSR),又能享有流暢的互動體驗(CSR)。
延伸閱讀
- Next.js Refine 教學文件 — Next.js + Refine 的入門教學。
- Next.js + Refine 部分更新完整指南 — 在 Client Component 中如何用 Refine Mutation 做局部更新,而非整頁刷新。