🎭 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):在浏览器中完全渲染
推荐的选择策略
- 性能最佳:尽可能使用SSG或ISR
- 实时数据:对于频繁变化的内容,使用SSR或ISR
- 交互性强:对于需要大量客户端交互的页面,使用CSR
- 最佳实践:在同一个应用中混合使用多种渲染方式
性能提示
- 尽量减少客户端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!