魯斯前端布魯斯前端

文章中英模式

常見的前端面試題目 - 頁面載入 - 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後發送到瀏覽器
  • 更好的首次內容呈現時間
  • SEO 友好
  • 低端裝置友善
  • 適合動態內容
  • 伺服器負載高
  • TTFB 較長
  • 完整頁面刷新
  • 開發複雜度高
  • Amazon、Shopify (電商)
  • CNN、BBC (新聞網站)
  • Airbnb、Booking.com
SSG(靜態網站生成)建置時預先渲染成靜態HTML檔案
  • 超快的頁面載入速度
  • 優異的 SEO
  • 低成本高安全性
  • 減輕伺服器壓力
  • 更新不即時
  • 不適合高度動態內容
  • 建置時間長
  • 開發複雜度
  • Next.js 文檔、MDN
  • Gatsby 部落格、Vercel
  • Stripe、Netlify 官網
CSR(客戶端渲染)瀏覽器用JS動態生成頁面內容
  • 豐富的互動體驗
  • 快速頁面切換
  • 減輕伺服器負載
  • 前後端完全分離
  • 首次載入較慢
  • SEO 挑戰
  • 低效能裝置上表現差
  • 可能出現白屏問題
  • Gmail、Google Maps
  • Facebook、Twitter 後台
  • Spotify、Discord

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>
  );
}

🔥 常見面試題目

(一)如何選擇適合的渲染方式?

選擇渲染方式需考慮以下因素:

考慮因素SSRSSGCSR
內容更新頻率經常變動的內容(如社交媒體動態、新聞網站)較少更新的內容(如部落格、公司官網)需要即時互動的應用(如Gmail、Facebook應用)
SEO 需求高(搜尋引擎可直接爬取完整HTML)最佳(預先生成的頁面對搜尋引擎最友好)較差(搜尋引擎可能無法等待JS渲染完成)
使用者體驗快速看到內容,但可能有短暫互動延遲最快的首屏載入(如電商產品頁面)首次載入較慢,但後續頁面切換流暢(如單頁應用)
API 資料獲取伺服器安全獲取(如用戶個人資料頁面)建置時一次性獲取(如產品目錄頁面)瀏覽器中獲取(如天氣應用、股票看板)
技術資源需要較強伺服器(每個請求都要處理渲染)建置時需資源,但可部署到簡單靜態主機伺服器負擔輕,但用戶裝置需處理渲染

最佳實踐是根據專案需求混合使用不同渲染方式,例如 Next.js 允許在同一應用中針對不同頁面選擇不同的渲染策略。

(二)SSR 和 CSR 的首次載入有什麼區別?

渲染方式首次載入流程
SSR
  1. 瀏覽器發出 HTTP 請求
  2. 伺服器執行 React 代碼生成 HTML
  3. 瀏覽器接收完整 HTML 並立即顯示
  4. JavaScript 並行下載和解析
  5. Hydration 過程:JS 代碼接管現有 HTML 元素並添加事件處理
CSR
  1. 瀏覽器發出 HTTP 請求
  2. 伺服器返回基礎 HTML 殼(通常只有 root div)
  3. 瀏覽器下載 JavaScript bundle
  4. JavaScript 解析和執行
  5. React 創建虛擬 DOM 並渲染實際 DOM
  6. 頁面內容才最終顯示

關鍵差異: 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 時特別小心
  • 使用動態導入分離客戶端專有代碼