文章中英模式
常見的前端面試題目 - 頁面載入 - SSR vs SSG vs CSR 深入解析
深入解析前端三大渲染方式:SSR、SSG、CSR 的運作原理、優缺點比較,以及 React Server Components 和 Hydration 機制的完整剖析。
文章中英模式

懶得看文章?那就來看影片吧
基本概念
現代前端開發有三種主要的頁面渲染方式:伺服器端渲染(SSR)、靜態網站生成(SSG)和客戶端渲染(CSR/SPA)。每種方式各有優缺點,適用於不同的應用場景。
使用者請求 URL
│
↓
根據渲染方式處理
│
├─→ SSR: 每次請求在伺服器處理邏輯,即時渲染
│ └─→ 伺服器生成 HTML → 返回完整頁面
│
├─→ SSG: 建置(npm run build)時在伺服器處理獲取數據等邏輯,預先生成好HTML
│ └─→ 返回靜態 HTML → CDN 快取
│
└─→ CSR: 瀏覽器渲染
└─→ 返回空白 HTML(index.html裡僅有<div id="root"></div>) → 執行 JS → 渲染頁面(將頁面掛載到div id="root"上)簡易代碼示例
SSR 示例 (Next.js 15)
// app/posts/[id]/page.tsx
// 動態路由頁面,每次請求時在伺服器端渲染
export default async function Post({ params }) {
// 伺服器端獲取資料,每次請求都執行
const res = await fetch(`https://api.example.com/posts/${params.id}`, {
cache: 'no-store' // 不快取,確保獲取最新資料
});
const post = await res.json();
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
<p>發布時間: {new Date(post.createdAt).toLocaleString()}</p>
</div>
);
}SSG 示例 (Next.js 15)
// app/blog/[slug]/page.tsx
// 靜態生成頁面,在建置時預先渲染(執行 npm run build 或類似指令時)
export async function generateStaticParams() {
// 建置時執行,決定哪些頁面要預先渲染
const posts = await fetch('https://api.example.com/posts').then(res =>
res.json()
);
return posts.map((post) => ({
slug: post.slug,
}));
}
export default async function BlogPost({ params }) {
// 建置時執行,為每個 slug 獲取資料
const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(res =>
res.json()
);
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
<p>最後更新: {new Date(post.updatedAt).toLocaleDateString()}</p>
</article>
);
}CSR 示例 (React)
// HTML 檔案中的空容器
// index.html
<html>
<head>
<title>CSR 應用</title>
</head>
<body>
<div id="root"></div>
<script src="app.js"></script>
</body>
</html>
// 客戶端渲染組件 (app.js)
import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom/client';
function Page() {
const [data, setData] = useState(null);
// 瀏覽器中獲取資料
useEffect(() => {
async function fetchData() {
const response = await fetch('https://api.example.com/data');
const result = await response.json();
setData(result);
}
fetchData();
}, []);
// 等待資料載入
if (!data) return <div>Loading...</div>;
// 資料載入後渲染
return <div>{data.title}</div>;
}
// 將 React 應用注入到 HTML 的空容器中
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Page />);渲染流程比較
┌─────────────────────────────────────────────────────────────────────┐
│ SSR 流程 │
└─────────────────────────────────────────────────────────────────────┘
使用者請求 → 伺服器渲染HTML → 返回完整HTML → 瀏覽器顯示 → JS加載 → 互動
┌─────────────────────────────────────────────────────────────────────┐
│ SSG 流程 │
└─────────────────────────────────────────────────────────────────────┘
建置時間: 預先渲染所有頁面 → 生成靜態HTML檔案 (執行 npm run build 時)
使用者請求 → CDN返回靜態HTML → 瀏覽器顯示 → JS加載 → 互動
┌─────────────────────────────────────────────────────────────────────┐
│ CSR 流程 │
└─────────────────────────────────────────────────────────────────────┘
使用者請求 → 返回最小HTML → 加載JS → JS執行渲染 → 頁面可見 → 互動┌─────────────────────────────────────────────────────────────────────┐
│ 時間軸比較 │
└─────────────────────────────────────────────────────────────────────┘
時間 ─────────────────────────────────────────────────────────────────▶
SSR: [伺服器渲染]───[首次內容顯示]───[JS加載]───[完全互動]
└─TTFB較長─┘ └─FCP較快─┘ └─TTI─┘
SSG: [CDN響應]───────[首次內容顯示]───[JS加載]───[完全互動]
└─TTFB極短─┘ └─FCP極快─┘ └─TTI─┘
CSR: [加載最小HTML]──[加載JS]───[渲染DOM]───[首次內容顯示+互動]
└─TTFB短─┘ └─大量JS─┘└─白屏─┘ └─FCP較慢但同時TTI─┘
TTFB: Time To First Byte FCP: First Contentful Paint TTI: Time To Interactive前端渲染方式比較
| 渲染方式 | 工作原理 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|---|
| SSR(伺服器端渲染) | 伺服器生成HTML後發送到瀏覽器 |
|
|
|
| SSG(靜態網站生成) | 建置時預先渲染成靜態HTML檔案 |
|
|
|
| CSR(客戶端渲染) | 瀏覽器用JS動態生成頁面內容 |
|
|
|
React Server Components 與 Hydration
React Server Components (RSC)
React Server Components 是 React 團隊推出的新功能,它讓我們可以選擇哪些組件只在伺服器上運行,而不需要發送到瀏覽器。這解決了傳統 SSR 的一個大問題:以前所有組件都必須在伺服器和瀏覽器兩邊都能運行。
React Server Components 的工作方式
│
├─→ 伺服器端組件
│ ├─→ 不會發送 JavaScript 到瀏覽器,只傳送靜態 HTML
│ ├─→ 可以直接連接資料庫、讀取檔案
│ └─→ 幫助減少瀏覽器下載的代碼量
│
└─→ 瀏覽器端
├─→ 接收伺服器組件的渲染結果(純 HTML)
└─→ 與瀏覽器組件無縫結合主要好處:
- •減少 JavaScript 下載量:伺服器組件的代碼不會發送到瀏覽器,瀏覽器只收到渲染後的 HTML,大幅減輕網路傳輸負擔
- •直接訪問後端資源:伺服器組件可以直接連接資料庫、讀取檔案系統或使用伺服器端 API,無需透過前端 API 請求再獲取資料
- •自動分割代碼:系統會自動決定哪些組件在伺服器運行,簡化開發
- •分批傳輸 UI:透過 React Suspense 和 Streaming SSR 技術,可以先傳送頁面的重要部分(如導航欄、主要內容),再傳送次要部分(如評論區、相關產品),例如:
React Server Components 與 Suspense 結合使用
ExpensiveDataComponent 可以是伺服器組件,在伺服器獲取資料並渲染
瀏覽器 伺服器
┌────────────┐ ┌────────────────────────┐
│ 顯示Loading │◄── 先傳送 ──┤ <Suspense fallback={...}> │
└─────┬──────┘ │ │
│ │ RSC 在伺服器獲取資料 │
│ │ <ExpensiveDataComponent/> │
┌─────▼──────┐ │ │
│ 顯示完整內容 │◄── 後傳送 ──┤ 傳送渲染好的HTML和數據 │
└────────────┘ └────────────────────────┘現實中使用 RSC 的產品:
- •Next.js 13+:完整整合了 RSC,App Router 模式下默認使用 Server Components
- •Shopify 商店:使用 Hydrogen 框架(基於 RSC)構建的電商網站,提供更快的產品頁面載入
- •Vercel 平台:其官方網站和儀表板使用 RSC 優化性能
實際應用場景:
// 伺服器組件範例 - 只在伺服器運行,不發送JS到瀏覽器
// ProductPage.js
async function ProductPage({ productId }) {
// 直接訪問資料庫,這段代碼不會發送到瀏覽器
const product = await db.products.findUnique({
where: { id: productId }
});
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<img src={product.imageUrl} alt={product.name} />
{/* 客戶端組件,會發送JS到瀏覽器 */}
{/* 注意:實際使用時,AddToCartButton 組件檔案需要在頂部添加 "use client" 指令 */}
<AddToCartButton productId={product.id} />
{/* 在 Next.js 中,客戶端組件必須在檔案頂部添加 "use client" 指令 */}
</div>
);
}Hydration 過程
Hydration 是將伺服器渲染的靜態 HTML 轉換為可互動 React 應用的過程。
// 伺服器端渲染 HTML
import { renderToString } from 'react-dom/server';
function App() {
return (
<div>
<h1>Hello World</h1>
<button onClick={() => alert('Clicked')}>Click Me</button>
</div>
);
}
// 生成靜態 HTML 字串
const html = renderToString(<App />);// 客戶端 Hydration 過程
import { hydrateRoot } from 'react-dom/client';
// 注入互動性,匹配現有 DOM 結構並添加事件監聽器
hydrateRoot(
document.getElementById('root'),
<App />
);處理客戶端特定功能:
function UserProfile() {
// 用於追蹤是否已在客戶端 hydrate
const [isClient, setIsClient] = useState(false);
// 僅在客戶端執行
useEffect(() => {
setIsClient(true);
}, []);
// 伺服器和初始 hydration 時顯示
if (!isClient) {
return <div>Loading profile...</div>;
}
// 僅在客戶端 hydration 後顯示
return (
<div>
<h2>User Profile</h2>
<button onClick={() => alert('Profile updated')}>
Update Profile
</button>
</div>
);
}🔥 常見面試題目
(一)如何選擇適合的渲染方式?
選擇渲染方式需考慮以下因素:
| 考慮因素 | SSR | SSG | CSR |
|---|---|---|---|
| 內容更新頻率 | 經常變動的內容(如社交媒體動態、新聞網站) | 較少更新的內容(如部落格、公司官網) | 需要即時互動的應用(如Gmail、Facebook應用) |
| SEO 需求 | 高(搜尋引擎可直接爬取完整HTML) | 最佳(預先生成的頁面對搜尋引擎最友好) | 較差(搜尋引擎可能無法等待JS渲染完成) |
| 使用者體驗 | 快速看到內容,但可能有短暫互動延遲 | 最快的首屏載入(如電商產品頁面) | 首次載入較慢,但後續頁面切換流暢(如單頁應用) |
| API 資料獲取 | 伺服器安全獲取(如用戶個人資料頁面) | 建置時一次性獲取(如產品目錄頁面) | 瀏覽器中獲取(如天氣應用、股票看板) |
| 技術資源 | 需要較強伺服器(每個請求都要處理渲染) | 建置時需資源,但可部署到簡單靜態主機 | 伺服器負擔輕,但用戶裝置需處理渲染 |
最佳實踐是根據專案需求混合使用不同渲染方式,例如 Next.js 允許在同一應用中針對不同頁面選擇不同的渲染策略。
(二)SSR 和 CSR 的首次載入有什麼區別?
| 渲染方式 | 首次載入流程 |
|---|---|
| SSR |
|
| CSR |
|
關鍵差異: SSR 更快顯示首次內容,但互動需等待 JS 加載完成;CSR 初始顯示較慢,但加載完成後頁面切換更流暢。SSR 的首次內容呈現時間(FCP)表現更好,而 CSR 在完全加載前可能顯示加載狀態或白屏。
(三)什麼是 Hydration Mismatch?如何解決?
Hydration Mismatch 是指伺服器渲染的 HTML 與客戶端渲染結果不一致的問題。
常見原因:
- •依賴客戶端環境的代碼(瀏覽器 API、window 對象)
- •依賴時間的計算(new Date())
- •隨機數(Math.random())
- •語言和時區差異
實例問題與解決方案:
// 不好的做法 - 可能導致 Hydration Mismatch
function DateDisplay() {
// 伺服器和客戶端可能顯示不同時間
return <div>{new Date().toLocaleString()}</div>;
}
// 好的做法 - 正確處理時間顯示
function DateDisplay() {
const [date, setDate] = useState(() => new Date());
useEffect(() => {
// 客戶端 hydration 後更新時間
setDate(new Date());
}, []);
return <div>{date.toLocaleString()}</div>;
}防止 Hydration Mismatch 的策略:
- •使用
useEffect處理客戶端專屬邏輯 - •使用
dangerouslySetInnerHTML時特別小心 - •使用動態導入分離客戶端專有代碼