鲁斯前端布鲁斯前端

文章中英模式

常见的前端面试题目 - 页面加载 - 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 时特别小心
  • 使用动态导入分离客户端专有代码