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