跳至正文

🎭 CSR vs SSR vs SSG

📖 定义

网页的生成和展示方式主要分为三种:

  • CSR (Client-Side Rendering): 使用 JavaScript 在浏览器中生成页面
  • SSR (Server-Side Rendering): 在服务器上完成 HTML 并发送
  • SSG (Static Site Generation): 在构建时预先生成 HTML

每种方式都有其优缺点,需要根据项目特性选择。

🎯 通过比喻理解

餐厅比喻

将三种渲染方式比作餐厅:

CSR (Client-Side Rendering)
= 自助烹饪餐厅 🍳

顾客(浏览器):
1. 进入餐厅(接收空的 HTML)
2. 获取食材(下载 JavaScript)
3. 亲自烹饪(渲染)
4. 享用完成的食物

优点:后厨(服务器)负担轻,可自由烹饪
缺点:需要烹饪时间,对新手来说较困难

---

SSR (Server-Side Rendering)
= 普通餐厅 🍽️

后厨(服务器):
1. 接受点单(请求)
2. 烹饪(生成 HTML)
3. 上菜(传输 HTML)

顾客(浏览器):
可以立即就餐!

优点:初始加载快,搜索引擎友好
缺点:后厨负担大,每次都需要烹饪

---

SSG (Static Site Generation)
= 便当连锁店 🍱

构建时:
1. 提前准备所有便当
2. 存放在冰箱(CDN)

顾客(浏览器):
1. 下单
2. 立即获取打包好的便当
3. 食用

优点:超高速配送,成本低
缺点:难以更改菜单,新鲜度有限

建筑比喻

CSR = 组装家具 (宜家) 🪑
- 运送零件和说明书
- 在家自行组装
- 运费低
- 需要组装时间

SSR = 定制家具 🛋️
- 定制制作
- 交付成品
- 可立即使用
- 成本高,耗时

SSG = 成品家具 🪟
- 提前制作
- 存放在仓库
- 即时配送
- 便宜且快速
- 无法定制

⚙️ 工作原理

1. CSR (Client-Side Rendering)

// CSR 流程

// 第1阶段:服务器发送最小 HTML
// index.html
<!DOCTYPE html>
<html>
<head>
<title>CSR App</title>
</head>
<body>
<div id="root"></div> <!-- 空的! -->
<script src="/bundle.js"></script>
</body>
</html>

// 第2阶段:浏览器下载 JavaScript
// GET /bundle.js (2MB)

// 第3阶段:执行 JavaScript
ReactDOM.render(<App />, document.getElementById('root'));

// 第4阶段:调用 API 获取数据
fetch('/api/posts')
.then(res => res.json())
.then(data => setPost(data));

// 第5阶段:渲染完成

// 时间线:
// 0ms: HTML 到达(空白屏幕)
// 100ms: 开始下载 JavaScript
// 1000ms: JavaScript 解析和执行
// 1500ms: 调用 API
// 2000ms: 数据到达
// 2100ms: 屏幕显示完成 ✅

// 用户看到的:
// 0-2100ms: 白屏或加载动画
// 2100ms~: 完整页面

2. SSR (Server-Side Rendering)

// SSR 流程

// 第1阶段:客户端请求
// GET /posts/123

// 第2阶段:服务器获取数据
// server.js (Node.js + Express)
app.get('/posts/:id', async (req, res) => {
// 查询数据库
const post = await db.posts.findById(req.params.id);

// 将 React 组件转换为 HTML 字符串
const html = ReactDOMServer.renderToString(
<PostPage post={post} />
);

// 发送完整的 HTML
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>${post.title}</title>
</head>
<body>
<div id="root">${html}</div>
<script>
window.__INITIAL_DATA__ = ${JSON.stringify(post)};
</script>
<script src="/bundle.js"></script>
</body>
</html>
`);
});

// 第3阶段:浏览器接收 HTML
// 可立即显示屏幕!(HTML 已完成)

// 第4阶段:注水(Hydration)
// JavaScript 加载后连接事件监听器
ReactDOM.hydrate(<PostPage />, document.getElementById('root'));

// 时间线:
// 0ms: 请求
// 50ms: 服务器查询数据
// 100ms: 生成 HTML
// 200ms: HTML 到达
// 250ms: 显示屏幕 ✅ (快!)
// 1000ms: 加载 JavaScript
// 1100ms: 注水完成(可交互)

// 用户看到的:
// 0-250ms: 加载
// 250-1100ms: 可见但不可点击
// 1100ms~: 完全可用

3. SSG (Static Site Generation)

// SSG 流程

// 构建时(部署前)
// next build

// 第1阶段:预先生成所有页面
// pages/posts/[id].js
export async function getStaticPaths() {
// 要生成的页面列表
const posts = await db.posts.findAll();

return {
paths: posts.map(post => ({
params: { id: post.id.toString() }
})),
fallback: false
};
}

export async function getStaticProps({ params }) {
// 每个页面的数据
const post = await db.posts.findById(params.id);

return {
props: { post }
};
}

// 第2阶段:生成 HTML 文件
// .next/server/pages/posts/1.html
// .next/server/pages/posts/2.html
// .next/server/pages/posts/3.html
// ...

// 第3阶段:部署到 CDN
// Vercel, Netlify, CloudFront 等

// 运行时(用户请求时)
// GET /posts/123

// CDN 立即返回 HTML(超快!)

// 时间线:
// 0ms: 请求
// 10ms: CDN 返回 HTML ✅ (非常快!)
// 500ms: 加载 JavaScript
// 600ms: 注水完成

// 用户看到的:
// 0-10ms: 加载
// 10-600ms: 可见但不可点击
// 600ms~: 完全可用

💡 实际示例

CSR 示例 (Create React App)

// src/App.js
import React, { useState, useEffect } from 'react';

function App() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
// 在浏览器中调用 API
fetch('https://api.example.com/posts')
.then(res => res.json())
.then(data => {
setPosts(data);
setLoading(false);
})
.catch(error => {
console.error('Error:', error);
setLoading(false);
});
}, []);

if (loading) {
return <div>加载中...</div>;
}

return (
<div className="app">
<h1>博客文章</h1>
<ul>
{posts.map(post => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</li>
))}
</ul>
</div>
);
}

export default App;

// 优点:
// 1. 服务器负担轻
// 2. 页面切换快(SPA)
// 3. 丰富的交互性

// 缺点:
// 1. 初始加载慢
// 2. SEO 困难(空 HTML)
// 3. 必须有 JavaScript

// 适用场景:
// - 管理员仪表盘
// - 登录后使用的应用
// - 不需要 SEO 的服务

SSR 示例 (Next.js)

// pages/posts/[id].js
import { useRouter } from 'next/router';

function Post({ post }) {
const router = useRouter();

// 加载状态(fallback)
if (router.isFallback) {
return <div>加载中...</div>;
}

return (
<article>
<h1>{post.title}</h1>
<p>{post.author}</p>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}

// 在服务器上执行(每次请求)
export async function getServerSideProps({ params }) {
// 调用数据库或 API
const res = await fetch(`https://api.example.com/posts/${params.id}`);
const post = await res.json();

// 404 处理
if (!post) {
return {
notFound: true
};
}

// 传递 props
return {
props: {
post
}
};
}

export default Post;

// 服务器生成的 HTML:
// <!DOCTYPE html>
// <html>
// <head>
// <title>Next.js</title>
// </head>
// <body>
// <div id="__next">
// <article>
// <h1>标题</h1>
// <p>作者:张三</p>
// <div>
// <p>正文内容...</p>
// </div>
// </article>
// </div>
// <script src="/_next/static/chunks/main.js"></script>
// </body>
// </html>

// 优点:
// 1. 初始加载快
// 2. SEO 优化
// 3. 实时数据

// 缺点:
// 1. 服务器成本高
// 2. 响应时间波动
// 3. 缓存困难

// 适用场景:
// - 新闻网站
// - 社交媒体动态
// - 需要实时更新的场景
// Next.js 支持所有渲染方式:

// 1. getServerSideProps() - SSR
export async function getServerSideProps(context) {
return {
props: {
// 每次请求时动态获取的数据
}
};
}

// 2. getStaticProps() - SSG
export async function getStaticProps() {
return {
props: {
// 构建时获取的静态数据
},
// 重新验证间隔(可选)
revalidate: 60 // ISR: 每60秒重新生成
};
}

// 3. getStaticPaths() - SSG 动态路由
export async function getStaticPaths() {
return {
paths: [
// 预渲染的动态路由
],
fallback: 'blocking' // 其他路由按需渲染
};
}

// 4. useState/useEffect() - CSR
function ClientComponent() {
const [data, setData] = useState(null);

useEffect(() => {
// 仅在客户端执行
fetch('/api/data')
.then(res => res.json())
.then(setData);
}, []);

return <div>{/* 客户端渲染内容 */}</div>;
}

Next.js渲染方式比较

Next.js是最灵活的Web框架,允许在同一个项目中混合使用不同的渲染策略:

  • 服务器端渲染(SSR):每次请求时动态生成
  • 静态站点生成(SSG):构建时生成静态HTML
  • 增量静态再生成(ISR):定期重新生成静态页面
  • 客户端渲染(CSR):在浏览器中完全渲染

推荐的选择策略

  1. 性能最佳:尽可能使用SSG或ISR
  2. 实时数据:对于频繁变化的内容,使用SSR或ISR
  3. 交互性强:对于需要大量客户端交互的页面,使用CSR
  4. 最佳实践:在同一个应用中混合使用多种渲染方式

性能提示

  • 尽量减少客户端JavaScript的大小
  • 使用代码分割(Code Splitting)
  • 优化图像和资源加载
  • 利用Next.js的自动优化功能

结论

没有一种渲染方式适用于所有场景。选择渲染方式时,需要考虑:

  • 性能需求
  • SEO要求
  • 数据更新频率
  • 用户交互复杂度

建议:针对不同页面使用最合适的渲染策略,而不是在整个应用中使用单一方法。

SSG 示例 (Next.js)

// Next.js - 混合框架

// 1. SSG (默认)
// pages/about.js
function About() {
return <div>公司介绍</div>;
}

export default About;
// 没有 getStaticProps 时自动 SSG

// 2. 带数据的 SSG
// pages/blog/[slug].js
export async function getStaticPaths() {
return {
paths: [{ params: { slug: 'hello' }}],
fallback: false
};
}

export async function getStaticProps({ params }) {
const post = await getPost(params.slug);
return { props: { post }};
}

// 3. ISR (增量静态重新生成)
export async function getStaticProps() {
return {
props: { data: '...' },
revalidate: 60 // 每 60 秒重新生成
};
}

// 4. SSR
// pages/news.js
export async function getServerSideProps() {
const news = await getLatestNews();
return { props: { news }};
}

// 5. CSR
// pages/dashboard.js
function Dashboard() {
const { data } = useSWR('/api/user', fetcher);
return <div>{data}</div>;
}

// 6. API Routes (无服务器函数)
// pages/api/hello.js
export default function handler(req, res) {
res.status(200).json({ message: 'Hello' });
}

// Next.js 的优点:
// - 基于文件的路由
// - 自动代码分割
// - 图像优化
// - TypeScript 支持
// - 快速刷新
// - Vercel 部署优化

Q3. 什么是 Hydration?

A: 在 SSR/SSG 中将静态 HTML 转换为交互式的过程:

// Hydration 过程

// 第1步:在服务器上生成 HTML
// server.js
const html = ReactDOMServer.renderToString(<App />);

// 生成的 HTML:
<div id="root">
<button>点击 (0)</button>
</div>

// 第2步:传递到浏览器
// 用户可以立即看到!
// 但按钮无法点击(没有事件监听器)

// 第3步:加载并执行 JavaScript
// client.js
ReactDOM.hydrate(<App />, document.getElementById('root'));

// Hydration 过程中:
// 1. React 分析 DOM 树
// 2. 与虚拟 DOM 比较
// 3. 连接事件监听器
// 4. 初始化状态

// 第4步:Hydration 完成
// 现在可以点击按钮了!变得交互式!

// Hydration 问题:
// 不匹配警告
const MismatchComponent = () => {
const [mounted, setMounted] = useState(false);

useEffect(() => {
setMounted(true);
}, []);

return (
<div>
{/* ❌ 服务器:"服务器",客户端:"客户端" */}
{typeof window === 'undefined' ? '服务器' : '客户端'}

{/* ✅ 仅在 Hydration 后渲染 */}
{mounted && <ClientOnlyComponent />}
</div>
);
};

// Hydration 优化:
// 1. 减小初始 HTML 大小
// 2. 减小 JavaScript 包大小
// 3. 内联关键 CSS
// 4. 仅 Hydrate 必要部分(渐进式 Hydration)

Q4. 可以解决 CSR 的 SEO 问题吗?

A: 有几种方法,但并非完美:

// CSR SEO 改进方法

// 1. 预渲染(构建时生成 HTML)
// 使用 react-snap
// package.json
{
"scripts": {
"postbuild": "react-snap"
}
}

// 爬虫访问时提供预生成的 HTML
// 实际用户使用 CSR

// 2. 服务器端渲染服务
// 使用 Prerender.io, Rendertron 等服务

// nginx 配置
location / {
if ($http_user_agent ~* "googlebot|bingbot|yandex") {
proxy_pass http://prerender-service;
}
}

// 3. Google Search Console
// - 测试 JavaScript 渲染
// - 检查 robots.txt
// - 提交 sitemap.xml

// 4. 动态更新 Meta 标签
import { Helmet } from 'react-helmet';

function BlogPost({ post }) {
return (
<>
<Helmet>
<title>{post.title}</title>
<meta name="description" content={post.excerpt} />
<meta property="og:title" content={post.title} />
<meta property="og:image" content={post.image} />
</Helmet>
<article>{/* ... */}</article>
</>
);
}

// 5. 结构化数据(JSON-LD)
<script type="application/ld+json">
{`
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "${post.title}",
"author": "${post.author}",
"datePublished": "${post.date}"
}
`}
</script>

// 局限性:
// - 完美的 SEO 是不可能的
// - 复杂且难以维护
// - SSR/SSG 更好

// 结论: 如果 SEO 很重要,使用 SSR/SSG!

Q5. 如何测量渲染性能?

A: 可以使用多种工具测量:

// 1. Lighthouse(Chrome DevTools)
// 谷歌 DevTools > Lighthouse 选项卡
// - 性能
// - 可访问性
// - 最佳实践
// - SEO

// 关键指标:
const webVitals = {
FCP: 'First Contentful Paint', // 首次内容绘制
LCP: 'Largest Contentful Paint', // 最大内容绘制
FID: 'First Input Delay', // 首次输入延迟
TTI: 'Time to Interactive', // 可交互时间
TBT: 'Total Blocking Time', // 总阻塞时间
CLS: 'Cumulative Layout Shift' // 累积布局偏移
};

// 2. web-vitals 库
import { getCLS, getFID, getFCP, getLCP, getTTI } from 'web-vitals';

function sendToAnalytics({ name, value, id }) {
console.log(name, value);
// 发送到 Google Analytics
gtag('event', name, {
event_category: 'Web Vitals',
value: Math.round(value),
event_label: id,
non_interaction: true
});
}

getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getFCP(sendToAnalytics);
getLCP(sendToAnalytics);
getTTI(sendToAnalytics);

// 3. Next.js Analytics
// pages/_app.js
export function reportWebVitals(metric) {
console.log(metric);
// {
// name: 'FCP',
// value: 1234.5,
// id: 'v2-1234567890'
// }
}

// 4. Chrome DevTools 性能
// 1. DevTools > 性能选项卡
// 2. 开始录制
// 3. 重新加载页面
// 4. 停止录制
// 5. 分析:
// - 加载: 下载 HTML, CSS, JS
// - 脚本执行: JavaScript 执行
// - 渲染: 布局, 绘制
// - 绘制: 绘制像素

// 5. Network 选项卡
// - 资源加载时间
// - 文件大小
// - 瀑布图
// - 并行下载

// 6. React DevTools Profiler
import { Profiler } from 'react';

function onRenderCallback(
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime
) {
console.log(`${id} (${phase}) took ${actualDuration}ms`);
}

<Profiler id="App" onRender={onRenderCallback}>
<App />
</Profiler>

// 目标指标:
const goodMetrics = {
FCP: '< 1.8s',
LCP: '< 2.5s',
FID: '< 100ms',
TTI: '< 3.8s',
TBT: '< 200ms',
CLS: '< 0.1'
};

📖 下一步

如果你已经理解了渲染方式,可以继续学习:

  1. 什么是虚拟 DOM? - React 渲染的核心原理
  2. Web 性能优化(即将编写文档)- 性能改进方法
  3. SEO 基础(即将编写文档)- 搜索引擎优化

📽️ 总结

渲染方式对 Web 应用的性能和 SEO 有重大影响:

  • CSR:丰富的交互,SEO 困难
  • SSR:快速初始加载,服务器成本高
  • SSG:最佳性能,适合静态内容
  • ISR:SSG + 自动更新,最佳组合

根据项目特性选择渲染方式,必要时可以混合使用!