🎯 第一章:核心概念解析
1.1 CSR(客户端渲染)
// CSR = Client Side Rendering
// 页面在浏览器中通过 JavaScript 动态渲染
const csrFlow = {
1: '浏览器请求 HTML',
2: '服务器返回空壳 HTML + JS 链接',
3: '浏览器下载 JS',
4: 'JS 执行,动态生成内容',
5: '页面显示'
};
// 典型 CSR 应用:Create React App、Vue CLI
<!-- CSR 返回的 HTML 示例 -->
<!DOCTYPE html>
<html>
<head>
<title>CSR App</title>
</head>
<body>
<div id="root"></div> <!-- 空的容器 -->
<script src="bundle.js"></script> <!-- JS 负责渲染 -->
</body>
</html>
1.2 SSR(服务端渲染)
// SSR = Server Side Rendering
// 页面在服务器端渲染完成,直接返回 HTML
const ssrFlow = {
1: '浏览器请求页面',
2: '服务器执行渲染逻辑',
3: '服务器返回完整的 HTML',
4: '浏览器直接显示',
5: '然后加载 JS 进行 hydration(激活)'
};
// 典型 SSR 应用:Next.js、Nuxt.js
<!-- SSR 返回的 HTML 示例 -->
<!DOCTYPE html>
<html>
<head>
<title>SSR App</title>
</head>
<body>
<div id="root">
<!-- 已经渲染好的内容 -->
<h1>用户列表</h1>
<ul>
<li>张三</li>
<li>李四</li>
</ul>
</div>
<script src="bundle.js"></script> <!-- JS 负责交互 -->
</body>
</html>
1.3 同构渲染(Universal Rendering)
// 同构渲染 = SSR + CSR 的结合
// 同一套代码,既能在服务端运行,也能在客户端运行
const isomorphicFlow = {
1: '服务端执行代码,渲染出 HTML',
2: '返回完整 HTML,用户立刻看到内容',
3: '浏览器下载 JS',
4: 'JS 在客户端再次执行(hydrate)',
5: '页面变成可交互的 SPA'
};
// 关键概念:hydration(水合/激活)
// 客户端 JS 接管服务端渲染的静态页面,添加事件监听
🔗 第二章:联系
2.1 关系图谱
const relationship = {
// CSR 和 SSR 是渲染方式的两种选择
rendering: {
csr: '客户端渲染',
ssr: '服务端渲染',
isomorphic: '同构(两者结合)'
},
2.2 数据流向对比
// CSR
const csrWithSOA = {
1: '浏览器请求 HTML',
2: '返回空壳 HTML',
3: '浏览器加载 JS',
4: 'JS 调用多个后端服务',
5: '服务 A 返回用户数据',
6: '服务 B 返回商品数据',
7: 'JS 组合数据,渲染页面'
};
// SSR
const ssrWithSOA = {
1: '浏览器请求页面',
2: '服务器调用多个后端服务',
3: '服务 A 返回用户数据',
4: '服务 B 返回商品数据',
5: '服务器组合数据,渲染 HTML',
6: '返回完整 HTML',
7: '浏览器显示,JS 激活'
};
2.3 同构的关键:hydration
// hydration 过程
const hydration = {
// 服务端渲染的 HTML
serverHTML: `
<button class="counter">0</button>
`,
// 客户端 JS 进行 hydration
clientJS: `
// 找到已有的 DOM 元素
const button = document.querySelector('.counter');
// 添加事件监听(不重新创建 DOM)
button.addEventListener('click', () => {
const count = parseInt(button.textContent) + 1;
button.textContent = count;
});
`,
// 注意:不是重新渲染,而是"激活"已有的 DOM
key: '复用服务端渲染的 DOM,只添加事件'
};
📊 第三章:详细对比
3.1 对比表
| 维度 | CSR | SSR | 同构渲染 |
|---|---|---|---|
| 首次内容显示 | 慢(需等 JS 加载执行) | 快(直接返回 HTML) | 快(服务端渲染) |
| SEO 友好 | ❌ 差(爬虫看不到内容) | ✅ 好 | ✅ 好 |
| 服务端压力 | 小(只返回静态文件) | 大(需要渲染) | 大(需要渲染) |
| 用户体验 | 交互后流畅(SPA) | 页面跳转有白屏 | 两者兼顾 |
| 开发复杂度 | 低 | 中 | 高 |
| 首屏加载时间 | 2-5秒 | 0.5-1秒 | 0.5-1秒 |
| TTI(可交互时间) | 慢 | 中 | 中 |
| 数据获取时机 | 客户端 | 服务端 | 服务端+客户端 |
3.2 性能指标对比
const performanceMetrics = {
// FCP (First Contentful Paint)
FCP: {
CSR: '2.5s+ (需等待 JS)',
SSR: '0.5s (直接返回 HTML)',
},
// TTI (Time to Interactive)
TTI: {
CSR: '3s+ (JS 执行完)',
SSR: '2s (HTML 显示后还需 JS 激活)',
},
// TTFB (Time to First Byte)
TTFB: {
CSR: '快(静态文件)',
SSR: '慢(需要服务端渲染)',
}
};
🎯 第四章:使用场景分析
4.1 CSR 适合的场景
const csrScenarios = {
// ✅ 适合
suitable: [
'后台管理系统',
'数据看板/仪表盘',
'在线编辑器',
'企业内部应用',
'需要复杂交互的SPA'
],
// ❌ 不适合
notSuitable: [
'内容型网站(博客、新闻)',
'电商首页',
'对SEO有强需求的网站',
'首屏加载时间敏感的应用'
],
// 典型例子
examples: {
dashboard: '蚂蚁金服 Ant Design Pro',
tool: 'Google Docs',
admin: '各种中后台系统'
}
};
4.2 SSR 适合的场景
const ssrScenarios = {
// ✅ 适合
suitable: [
'内容型网站',
'电商网站',
'新闻门户',
'博客',
'对SEO有要求的网站'
],
// ❌ 不适合
notSuitable: [
'复杂交互应用',
'实时性要求高的应用',
'服务器资源有限的应用'
],
// 典型例子
examples: {
ecommerce: '淘宝商品详情页',
content: '知乎问题页',
news: '今日头条'
}
};
4.3 同构渲染适合的场景
const isomorphicScenarios = {
// ✅ 适合
suitable: [
'需要SEO的复杂应用',
'内容+交互混合的网站',
'电商网站(列表+详情)',
'社区/论坛',
'企业官网'
],
// 需要权衡
tradeoffs: [
'服务器压力较大',
'开发复杂度高',
'需要处理水合不匹配'
],
// 典型例子
examples: {
nextjs: 'Next.js 应用',
nuxt: 'Nuxt.js 应用',
remix: 'Remix 应用'
}
};
🏗️ 第五章:架构选型指南
5.1 选型决策树
const decisionTree = {
question1: '是否需要 SEO?',
branches: {
yes: {
question2: '交互复杂度高吗?',
options: {
high: '同构渲染(Next.js/Nuxt)',
low: 'SSR 或 SSG'
}
},
no: {
question2: '首屏加载速度要求高吗?',
options: {
high: 'SSR 或 同构',
low: 'CSR'
}
}
},
question2: '服务器资源有限吗?',
impact: {
yes: '倾向 CSR(服务器压力小)',
no: '可考虑 SSR/同构'
},
question3: '团队技术栈?',
impact: {
react: 'Next.js(同构)',
vue: 'Nuxt.js(同构)',
angular: 'Angular Universal'
}
};
5.2 不同规模项目的选择
const projectScales = {
// 小型项目(个人博客、官网)
small: {
recommendation: 'CSR 或 SSG',
reason: '开发简单,维护成本低',
tech: 'Vite + Vue/React'
},
// 中型项目(电商、社区)
medium: {
recommendation: '同构渲染',
reason: '需要 SEO,也有交互',
tech: 'Next.js/Nuxt.js'
},
// 大型项目(复杂平台)
large: {
recommendation: '同构 + 微前端 + SOA',
reason: '复杂业务,多团队协作',
tech: 'Next.js + Module Federation + BFF'
},
// 内容型网站(博客、文档)
content: {
recommendation: 'SSG (Static Site Generation)',
reason: '内容不变,构建时生成',
tech: 'Next.js SSG, Gatsby'
}
};
🚀 第六章:实战代码示例
6.1 CSR 实现
// React CSR 示例
// App.js
function App() {
const [data, setData] = useState(null);
useEffect(() => {
// 客户端获取数据
fetch('/api/data')
.then(res => res.json())
.then(setData);
}, []);
if (!data) return <div>Loading...</div>;
return <div>{data.content}</div>;
}
// 构建产物
// index.html (简化版)
<!DOCTYPE html>
<div id="root"></div>
<script src="bundle.js"></script>
6.2 SSR 实现(Node.js + React)
// 服务端渲染代码
import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import App from './App';
const app = express();
app.get('/', async (req, res) => {
// 1. 获取数据
const data = await fetchData();
// 2. 渲染组件为 HTML
const html = renderToString(<App data={data} />);
// 3. 返回完整 HTML
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>SSR App</title>
</head>
<body>
<div id="root">${html}</div>
<script>
window.__INITIAL_DATA__ = ${JSON.stringify(data)};
</script>
<script src="bundle.js"></script>
</body>
</html>
`);
});
// 客户端 hydration
// client.js
import { hydrateRoot } from 'react-dom/client';
const data = window.__INITIAL_DATA__;
hydrateRoot(document.getElementById('root'), <App data={data} />);
6.3 同构的关键:数据同步
// 确保客户端和服务端数据一致
class IsomorphicApp {
// 服务端:获取数据并渲染
static async serverRender(req) {
const stores = {
userStore: await userStore.fetch(req.params.id),
postStore: await postStore.fetch()
};
const html = renderToString(
<Provider stores={stores}>
<App />
</Provider>
);
return {
html,
initialState: {
user: stores.userStore.getState(),
posts: stores.postStore.getState()
}
};
}
// 客户端:使用服务端数据初始化
static clientRender() {
// 使用服务端注入的数据
const initialState = window.__INITIAL_STATE__;
const stores = {
userStore: new UserStore(initialState.user),
postStore: new PostStore(initialState.posts)
};
hydrateRoot(
document.getElementById('root'),
<Provider stores={stores}>
<App />
</Provider>
);
}
}
🎓 第七章:难点
Q1:CSR 和 SSR 的主要区别是什么?
const answer = {
// 渲染位置
location: 'CSR在客户端渲染,SSR在服务端渲染',
// 首屏时间
FCP: 'SSR比CSR快,因为直接返回HTML',
// SEO
SEO: 'SSR对SEO友好,CSR不友好',
// 交互体验
interaction: 'CSR后续交互更流畅,SSR页面跳转有白屏',
// 服务器压力
serverLoad: 'SSR增加服务器压力,CSR服务器压力小',
// 开发复杂度
complexity: 'SSR比CSR复杂,需要处理同构问题'
};
Q2:什么是 hydration?为什么需要它?
const hydrationAnswer = {
// 定义
definition: 'hydration是客户端接管服务端渲染的静态页面,添加事件监听的过程',
// 为什么需要
why: '服务端只生成了静态HTML,没有事件交互,需要JS来"激活"',
// 过程
process: `
1. 服务端返回完整的HTML,用户立刻看到内容
2. 浏览器下载JS bundle
3. JS执行,遍历已有的DOM树
4. 为DOM元素添加事件监听
5. 页面变得可交互
`,
// 常见问题
issues: '如果服务端和客户端渲染结果不一致,会导致hydration失败'
};
Q3:同构渲染的常见问题集合?
同构渲染的理想与现实:
const ideal = {
// 理想情况
server: '服务端渲染出完整HTML,用户立刻看到内容',
client: '客户端无缝接管,保持SPA体验',
code: '同一套代码,两端运行'
};
const reality = {
// 实际情况
server: 'Node.js环境没有window/document',
client: '水合过程可能出错或闪烁',
code: '同一份代码,行为却可能不同'
};
1️⃣环境差异的坑:window/document 不存在
// ❌ 坑:直接在服务端使用浏览器API
function Component() {
// 服务端执行时会报错!
const width = window.innerWidth;
const element = document.getElementById('app');
return <div>宽度: {width}</div>;
}
// ✅ 解决方案1:动态导入(Next.js方式)
function Component() {
const [width, setWidth] = useState(null);
useEffect(() => {
// 只在客户端执行
setWidth(window.innerWidth);
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return <div>宽度: {width ?? '计算中...'}</div>;
}
// ✅ 解决方案2:条件判断
function SafeComponent() {
const isBrowser = typeof window !== 'undefined';
if (!isBrowser) {
// 服务端返回占位
return <div>加载中...</div>;
}
// 客户端才真正使用
return <div>宽度: {window.innerWidth}</div>;
}
// ✅ 解决方案3:Next.js 动态导入(不渲染服务端)
import dynamic from 'next/dynamic';
const ClientOnlyComponent = dynamic(
() => import('../components/WidthComponent'),
{ ssr: false } // 只在客户端渲染
);
2️⃣环境差异的坑:localStorage/sessionStorage 不存在
// ❌ 坑:服务端直接使用 localStorage
function UserInfo() {
// 服务端报错!localStorage is not defined
const token = localStorage.getItem('token');
return <div>Token: {token}</div>;
}
// ✅ 解决方案:封装存储服务
class StorageService {
static get(key) {
if (typeof window === 'undefined') {
return null; // 服务端返回null
}
return localStorage.getItem(key);
}
static set(key, value) {
if (typeof window === 'undefined') return;
localStorage.setItem(key, value);
}
}
// ✅ 或者使用状态管理 + useEffect
function TokenDisplay() {
const [token, setToken] = useState(null);
useEffect(() => {
setToken(localStorage.getItem('token'));
// 可选:监听变化
const handleStorage = () => {
setToken(localStorage.getItem('token'));
};
window.addEventListener('storage', handleStorage);
return () => window.removeEventListener('storage', handleStorage);
}, []);
return <div>{token ? `Token: ${token}` : '未登录'}</div>;
}
3️⃣环境差异的坑:服务端没有事件系统
// ❌ 坑:服务端绑定事件
function Button() {
// 服务端没有 addEventListener!
window.addEventListener('click', handleClick);
return <button>点击</button>;
}
// ✅ 正确做法
function CorrectButton() {
useEffect(() => {
// 只在客户端绑定
window.addEventListener('click', handleClick);
return () => window.removeEventListener('click', handleClick);
}, []);
return <button onClick={handleClick}>点击</button>;
// 注意:onClick 是 React 合成事件,不是原生事件
}
4️⃣数据不一致的坑:水合失败(Hydration Mismatch)
// ❌ 坑:服务端和客户端渲染结果不一致
function RandomComponent() {
// 服务端生成一个随机数
// 客户端生成另一个随机数
return <div>随机数: {Math.random()}</div>;
// 水合时会报错!Text content does not match server-rendered HTML
}
// ✅ 解决方案1:保持一致
function FixedRandom() {
const [random, setRandom] = useState(null);
useEffect(() => {
// 只在客户端生成
setRandom(Math.random());
}, []);
return <div>随机数: {random ?? '...'}</div>;
}
// ✅ 解决方案2:使用 stable 值
function StableComponent({ data }) {
// 使用服务端传递的数据
return <div>数据: {data.id}</div>;
}
5️⃣数据不一致的坑:时间戳不一致
// ❌ 坑:时间格式化结果不同
function DateComponent({ timestamp }) {
// 服务端和客户端时区可能不同
// 格式化结果可能不同
const date = new Date(timestamp).toLocaleString();
return <div>{date}</div>;
}
// ✅ 解决方案:统一格式化
function FixedDate({ timestamp }) {
const [formatted, setFormatted] = useState(null);
useEffect(() => {
// 客户端统一格式化
setFormatTimestamp(timestamp);
}, [timestamp]);
// 服务端返回 ISO 字符串
return <div>{formatted ?? new Date(timestamp).toISOString()}</div>;
}
// 或者使用专门的库处理时区
import { formatInTimeZone } from 'date-fns-tz';
function SafeDate({ timestamp, timezone = 'UTC' }) {
// 明确指定时区
return <div>{formatInTimeZone(timestamp, timezone, 'yyyy-MM-dd HH:mm:ss')}</div>;
}
6️⃣数据不一致的坑:数据预取不一致
// ❌ 坑:服务端和客户端数据不同
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
// 客户端重新获取数据
fetchUser(userId).then(setUser);
}, [userId]);
if (!user) return <div>Loading...</div>;
return <div>{user.name}</div>;
// 问题:服务端没数据,客户端有数据,水合失败
}
// ✅ 解决方案:服务端预取数据
// Next.js 方式
export async function getServerSideProps({ params }) {
const user = await fetchUser(params.id);
return {
props: {
user // 传递给组件
}
};
}
function UserProfile({ user }) {
// 服务端已经有数据,客户端直接用
return <div>{user.name}</div>;
}
7️⃣性能相关的坑:服务端渲染压力过大
// ❌ 坑:所有请求都走服务端渲染
// 每个用户请求都触发完整的渲染流程
// 高并发时服务器扛不住
// ✅ 解决方案1:缓存策略
// Next.js 的 ISR (Incremental Static Regeneration)
export async function getStaticProps() {
const data = await fetchData();
return {
props: { data },
revalidate: 60 // 60秒内走缓存
};
}
// ✅ 解决方案2:页面级缓存
const pageCache = new Map();
async function renderWithCache(req, res) {
const key = req.url;
if (pageCache.has(key)) {
const { html, expire } = pageCache.get(key);
if (Date.now() < expire) {
return res.send(html); // 返回缓存
}
}
// 重新渲染
const html = await renderToString(<App />);
pageCache.set(key, {
html,
expire: Date.now() + 60000 // 缓存1分钟
});
res.send(html);
}
8️⃣性能相关的坑:水合开销过大
// ❌ 坑:整个应用水合,导致 TTI 变慢
// 即使是大页面,也要等所有组件都hydrate完才能交互
// ✅ 解决方案1:渐进式水合
function HeavyComponent() {
const [shouldRender, setShouldRender] = useState(false);
useEffect(() => {
// 延迟水合
const timer = setTimeout(() => setShouldRender(true), 1000);
return () => clearTimeout(timer);
}, []);
if (!shouldRender) {
// 返回服务端渲染的静态HTML
return <div dangerouslySetInnerHTML={{ __html: serverHtml }} />;
}
return <ActualComponent />;
}
// ✅ 解决方案2:部分水合
// 使用 React 18 的 Selective Hydration
import { hydrateRoot } from 'react-dom/client';
import { lazy, Suspense } from 'react';
const Comments = lazy(() => import('./Comments'));
function Page() {
return (
<div>
<Header /> {/* 立即水合 */}
<Content /> {/* 立即水合 */}
<Suspense fallback={<div>加载评论...</div>}>
<Comments /> {/* 延迟水合 */}
</Suspense>
</div>
);
}
9️⃣性能相关的坑:内存泄漏
// ❌ 坑:服务端每次渲染都创建新对象,但未清理
const cache = new Map(); // 全局缓存,无限增长
function renderPage() {
// 每次渲染都添加,但从不删除
cache.set(Date.now(), largeData);
return renderToString(<App />);
}
// ✅ 解决方案:合理管理内存
class LRUCache {
constructor(maxSize = 100) {
this.maxSize = maxSize;
this.cache = new Map();
}
set(key, value) {
if (this.cache.size >= this.maxSize) {
// 删除最旧的
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, value);
}
get(key) {
const value = this.cache.get(key);
if (value) {
// 更新为最近使用
this.cache.delete(key);
this.cache.set(key, value);
}
return value;
}
}
🔟第三方库的坑:依赖 window 的库
// ❌ 坑:在服务端引入需要 window 的库
import * as d3 from 'd3'; // d3 内部使用了 window
// 服务端直接报错
// ✅ 解决方案1:动态导入
function Chart() {
useEffect(() => {
import('d3').then(d3 => {
// 在客户端使用
d3.select('#chart')....
});
}, []);
return <div id="chart" />;
}
// ✅ 解决方案2:NoSSR 包装
const NoSSR = ({ children }) => {
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
return isClient ? children : null;
};
function SafeChart() {
return (
<NoSSR>
<Chart />
</NoSSR>
);
}
🔟第三方库的坑:CSS-in-JS 服务端渲染问题
// ❌ 坑:CSS-in-JS 样式闪烁(FOUC)
// 服务端渲染的HTML没有样式,客户端注入后才出现
// ✅ 解决方案:使用支持SSR的CSS-in-JS
// styled-components 示例
import { ServerStyleSheet } from 'styled-components';
const sheet = new ServerStyleSheet();
try {
const html = renderToString(sheet.collectStyles(<App />));
const styles = sheet.getStyleTags(); // 获取样式标签
res.send(`
<!DOCTYPE html>
<html>
<head>${styles}</head>
<body>
<div id="root">${html}</div>
</body>
</html>
`);
} finally {
sheet.seal();
}
// ✅ 或者使用 CSS Modules(更简单)
import styles from './Button.module.css';
function Button() {
return <button className={styles.btn}>点击</button>;
}
// CSS 已构建好,服务端客户端一致
🔟路由相关的坑:服务端路由与客户端路由
// ❌ 坑:服务端不知道客户端路由变化
// 用户在前端切换路由,刷新页面后404
// ✅ 解决方案:配置 catch-all 路由
// Next.js 方式 - pages/[...slug].js
export default function CatchAll({ params }) {
return <div>路由: {params.slug.join('/')}</div>;
}
export async function getServerSideProps({ params }) {
// 服务端也能处理所有路由
return { props: { params } };
}
// 或者配置 nginx 返回 index.html
nginx`
location / {
try_files $uri /index.html;
}
`;
🔟路由相关的坑:重定向问题
// ❌ 坑:客户端重定向导致 SEO 问题
function PrivateRoute({ children }) {
const { user } = useAuth();
useEffect(() => {
if (!user) {
router.push('/login'); // 客户端重定向
}
}, [user]);
return children;
}
// 搜索引擎爬虫看不到内容
// ✅ 解决方案:服务端重定向
// Next.js 方式
export async function getServerSideProps(context) {
const { user } = await getAuth(context);
if (!user) {
return {
redirect: {
destination: '/login',
permanent: false
}
};
}
return { props: { user } };
}