本章是卷 II 的开篇章节。读完本章,你将建立完整的渲染模型心智地图,理解每种渲染策略的工作原理、优缺点和适用场景。
4.1 CSR 的局限性
什么是 CSR(Client-Side Rendering)
CSR(客户端渲染) 是传统 React SPA 的渲染方式:
- 浏览器下载空的 HTML 骨架
- 下载并执行 JavaScript
- React 在浏览器中渲染 UI
- 发起 API 请求获取数据
- 更新 UI 显示数据
典型流程:
浏览器请求 → 服务器返回空 HTML → 下载 JS Bundle → 执行 JS → 渲染 UI → 请求数据 → 更新 UI
CSR 的 HTML 结构
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
</head>
<body>
<div id="root"></div>
<script src="/static/js/bundle.js"></script>
</body>
</html>
问题:HTML 中没有实际内容,只有一个空的 <div id="root"></div>。
CSR 的局限性
1. 首屏白屏时间长
时间线:
0ms 用户访问页面
50ms 下载空 HTML
500ms 下载 JS Bundle(可能很大)
1000ms 执行 JavaScript
1500ms React 渲染 UI
2000ms 发起 API 请求
2500ms 数据返回,更新 UI
问题:用户在 2-3 秒内看到的是白屏或加载动画。
实际影响:
- 用户流失率高(每增加 1 秒加载时间,转化率下降 7%)
- 移动端网络慢,白屏时间更长
- 用户体验差,感觉"卡顿"
2. SEO 不友好
搜索引擎爬虫的工作方式:
Google Bot 访问页面
↓
下载 HTML
↓
尝试执行 JavaScript(成本高)
↓
等待 React 渲染(可能超时)
↓
抓取内容
问题:
- 爬虫看到的是空
<div id="root"></div> - 执行 JS 成本高,Google 可能不执行
- 某些爬虫(Bing、百度)不支持 JS 渲染
- 社交媒体预览(OG 标签)无法生成
SEO 影响:
- 搜索排名低
- 社交媒体分享无预览图
- 内容无法被索引
3. JavaScript Bundle 体积大
原因:
- 所有组件代码都打包到客户端
- 所有依赖库(React、Router、状态管理)都打包
- 即使用户只访问一个页面,也要下载整个应用的代码
示例:
bundle.js 大小:
- React + ReactDOM: 130KB
- React Router: 40KB
- Redux: 30KB
- 组件代码: 200KB
- 总计: 400KB(gzip 后约 120KB)
问题:
- 首屏加载慢
- 移动端流量消耗大
- 低端设备执行 JS 慢
4. 性能指标差
Core Web Vitals:
| 指标 | CSR 表现 | 原因 |
|---|---|---|
| LCP(Largest Contentful Paint) | 差(3-5s) | 需要等待 JS 执行和数据加载 |
| FCP(First Contentful Paint) | 差(2-3s) | 需要等待 JS 下载和执行 |
| TTI(Time to Interactive) | 差(4-6s) | 需要等待所有 JS 执行完成 |
| CLS(Cumulative Layout Shift) | 中 | 内容加载时可能布局偏移 |
什么时候用 CSR?
尽管 CSR 有局限性,但在某些场景下仍然是合适的选择:
| 场景 | 原因 |
|---|---|
| 内部管理系统 | 不需要 SEO,用户固定,可以接受加载时间 |
| 高度交互应用 | 如在线编辑器、游戏,需要实时响应 |
| 登录后页面 | 用户已登录,不需要 SEO |
| 实时数据应用 | 如股票行情、聊天室,数据频繁更新 |
4.2 SSR(Server-Side Rendering)
什么是 SSR
SSR(服务端渲染):每次请求时,服务端执行 React 组件,生成完整的 HTML,返回给浏览器。
流程:
浏览器请求 → 服务端执行 React → 生成 HTML → 返回完整 HTML → 浏览器显示 → Hydration(激活交互)
SSR 的工作原理
步骤 1:服务端渲染
// 服务端(Node.js)
import { renderToString } from 'react-dom/server';
import App from './App';
app.get('/', (req, res) => {
const html = renderToString(<App />);
res.send(`
<!DOCTYPE html>
<html>
<head><title>My App</title></head>
<body>
<div id="root">${html}</div>
<script src="/bundle.js"></script>
</body>
</html>
`);
});
步骤 2:浏览器接收 HTML
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
</head>
<body>
<div id="root">
<!-- 服务端生成的完整 HTML -->
<header>
<h1>Welcome to My App</h1>
<nav>
<a href="/about">About</a>
<a href="/contact">Contact</a>
</nav>
</header>
<main>
<p>This is the home page content.</p>
</main>
</div>
<script src="/bundle.js"></script>
</body>
</html>
用户立即看到内容,无需等待 JS 执行。
步骤 3:Hydration(注水)
// 浏览器
import { hydrateRoot } from 'react-dom/client';
import App from './App';
hydrateRoot(document.getElementById('root'), <App />);
Hydration 的作用:
- React 接管服务端生成的 HTML
- 添加事件监听器(
onClick、onChange) - 激活交互功能
- 建立状态管理
时间线:
0ms 用户访问页面
100ms 服务端渲染 HTML
200ms 返回完整 HTML
300ms 浏览器显示内容(用户已看到页面)
500ms 下载 JS Bundle
800ms Hydration 完成(页面可交互)
Next.js App Router 中的 SSR
在 App Router 中,使用动态渲染实现 SSR:
// app/page.tsx
export const dynamic = 'force-dynamic'; // 强制动态渲染(SSR)
async function getData() {
const res = await fetch('https://api.example.com/data', {
cache: 'no-store', // 不缓存,每次请求都获取最新数据
});
return res.json();
}
export default async function Home() {
const data = await getData();
return (
<div>
<h1>Dynamic Data</h1>
<p>Current time: {new Date().toISOString()}</p>
<ul>
{data.items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
关键点:
export const dynamic = 'force-dynamic':强制每次请求都渲染cache: 'no-store':不缓存 fetch 请求- 组件在服务端执行,生成 HTML
SSR 的优势
1. 首屏快
时间线对比:
| 阶段 | CSR | SSR |
|---|---|---|
| 下载 HTML | 50ms(空 HTML) | 200ms(完整 HTML) |
| 显示内容 | 2500ms(等待 JS + 数据) | 300ms(立即显示) |
| 可交互 | 2500ms | 800ms(Hydration) |
结果:SSR 首屏快 8 倍。
2. SEO 友好
搜索引擎爬虫看到的是完整 HTML:
<header>
<h1>Welcome to My App</h1>
<nav>
<a href="/about">About</a>
<a href="/contact">Contact</a>
</nav>
</header>
<main>
<p>This is the home page content.</p>
</main>
SEO 优势:
- 爬虫直接抓取内容,无需执行 JS
- 支持所有搜索引擎(Google、Bing、百度)
- 社交媒体预览正常(OG 标签)
3. 更好的 Core Web Vitals
| 指标 | CSR | SSR |
|---|---|---|
| LCP | 3-5s ❌ | 0.5-1s ✅ |
| FCP | 2-3s ❌ | 0.3-0.5s ✅ |
| TTI | 4-6s ❌ | 1-2s ✅ |
SSR 的劣势
1. 服务器负载高
问题:每次请求都要在服务端执行 React,消耗 CPU 和内存。
示例:
QPS(每秒请求数):1000
每次渲染耗时:50ms
CPU 使用率:50%(1000 * 50ms = 50s CPU 时间 / 秒)
解决方案:
- 使用 CDN 缓存
- 使用 ISR(增量静态再生)
- 使用 RSC(React Server Components)
2. TTFB 可能较高
TTFB(Time to First Byte):从请求到接收到第一个字节的时间。
影响因素:
- 服务端渲染时间
- 数据库查询时间
- API 调用时间
示例:
如果数据查询需要 500ms,TTFB 至少 500ms。
3. Hydration 成本
问题:浏览器需要下载 JS 并执行 Hydration,才能激活交互。
时间线:
300ms 显示内容(但不可交互)
800ms Hydration 完成(可交互)
用户体验:页面看起来已加载,但点击按钮无反应(“Uncanny Valley”)。
什么时候用 SSR?
| 场景 | 原因 |
|---|---|
| 个性化内容 | 每个用户看到不同内容(如 Dashboard) |
| 实时数据 | 数据频繁更新(如股票行情、新闻) |
| 需要 SEO | 内容页需要被搜索引擎索引 |
| 首屏性能要求高 | 电商、内容站等 |
4.3 SSG(Static Generation)
什么是 SSG
SSG(静态生成):在构建时(next build)预渲染所有页面,生成静态 HTML 文件。
流程:
构建时:执行 React → 生成 HTML → 保存为静态文件
请求时:CDN 直接返回静态 HTML → 浏览器显示 → Hydration
SSG 的工作原理
构建时
next build
Next.js 执行:
- 渲染每个页面
- 生成 HTML 文件
- 保存到
.next/server/app/
生成的文件:
.next/
└── server/
└── app/
├── index.html # /
├── about.html # /about
├── blog/
│ ├── index.html # /blog
│ └── hello.html # /blog/hello
└── ...
请求时
用户访问 /blog/hello
↓
CDN 返回 /blog/hello.html(静态文件)
↓
浏览器立即显示
↓
下载 JS → Hydration
Next.js App Router 中的 SSG
在 App Router 中,默认就是 SSG(除非使用了动态 API):
// app/blog/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts');
return res.json();
}
export default async function BlogPage() {
const posts = await getPosts();
return (
<div>
<h1>Blog</h1>
<ul>
{posts.map(post => (
<li key={post.id}>
<a href={`/blog/${post.slug}`}>{post.title}</a>
</li>
))}
</ul>
</div>
);
}
行为:
- 构建时执行
getPosts() - 生成静态 HTML
- 缓存到 CDN
动态路由的 SSG
对于动态路由,使用 generateStaticParams 预生成所有页面:
// app/blog/[slug]/page.tsx
async function getPost(slug: string) {
const res = await fetch(`https://api.example.com/posts/${slug}`);
return res.json();
}
// 告诉 Next.js 需要预生成哪些页面
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return posts.map(post => ({
slug: post.slug,
}));
}
export default async function BlogPost({
params,
}: {
params: { slug: string };
}) {
const post = await getPost(params.slug);
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
);
}
构建时:
generateStaticParams 返回:
[
{ slug: 'hello-world' },
{ slug: 'nextjs-guide' },
{ slug: 'react-tips' }
]
Next.js 预生成:
- /blog/hello-world.html
- /blog/nextjs-guide.html
- /blog/react-tips.html
SSG 的优势
1. 性能极佳
时间线:
0ms 用户访问页面
50ms CDN 返回静态 HTML(全球边缘节点)
100ms 浏览器显示内容
300ms Hydration 完成
原因:
- 静态文件存储在 CDN 边缘节点
- 无需服务端渲染
- 响应速度快(TTFB < 50ms)
2. 成本低
对比:
| 渲染方式 | 服务器成本 |
|---|---|
| SSR | 高(每次请求都渲染) |
| SSG | 低(只返回静态文件) |
示例:
SSR:1000 QPS * 50ms = 50s CPU 时间 / 秒
SSG:1000 QPS * 1ms = 1s CPU 时间 / 秒(CDN 直接返回)
3. 可扩展性强
CDN 分发:
用户在北京 → CDN 北京节点返回
用户在纽约 → CDN 纽约节点返回
用户在伦敦 → CDN 伦敦节点返回
优势:
- 全球低延迟
- 自动负载均衡
- 抗高并发
4. SEO 友好
和 SSR 一样,生成完整 HTML,搜索引擎可以直接抓取。
SSG 的劣势
1. 构建时间长
问题:如果有 10,000 个页面,构建时需要渲染所有页面。
示例:
10,000 个页面 * 100ms / 页面 = 1,000 秒 = 16 分钟
解决方案:
- 使用 ISR(增量静态再生)
- 按需生成(On-demand Revalidation)
2. 数据可能过时
问题:构建时生成的数据,可能已经过时。
示例:
构建时间:2025-01-01 10:00
用户访问:2025-01-02 10:00
数据已过期 24 小时
解决方案:
- 使用 ISR(定时再生)
- 使用 On-demand Revalidation(按需再生)
3. 不适合高度动态内容
问题:每次数据变化都要重新构建。
示例:
电商网站:
- 库存每秒变化
- 价格每分钟变化
- 不可能每次变化都重新构建
什么时候用 SSG?
| 场景 | 原因 |
|---|---|
| 博客文章 | 内容不常变化,需要 SEO |
| 文档站 | 内容稳定,页面数量多 |
| 营销页 | 需要极致性能和 SEO |
| 产品介绍页 | 内容相对固定 |
4.4 ISR(Incremental Static Regeneration)
什么是 ISR
ISR(增量静态再生):结合 SSG 和 SSR 的优势,页面在构建时生成,但可以定时或按需重新生成。
流程:
构建时:生成静态页面
请求时:
├─ 页面未过期 → 返回缓存的静态页面
└─ 页面已过期 → 返回缓存页面,后台重新生成
ISR 的工作原理
基于时间的 ISR
// app/blog/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 }, // 每 60 秒重新生成
});
return res.json();
}
export default async function BlogPage() {
const posts = await getPosts();
return (
<div>
<h1>Blog</h1>
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
行为:
- 首次请求:返回构建时生成的页面
- 60 秒内的请求:返回缓存页面(快速)
- 60 秒后的请求:
- 立即返回缓存页面(快速)
- 后台重新生成页面
- 下次请求使用新页面
时间线:
T+0s 构建时生成页面
T+10s 请求 → 返回缓存页面
T+30s 请求 → 返回缓存页面
T+60s 请求 → 返回缓存页面,后台重新生成
T+61s 后台生成完成
T+70s 请求 → 返回新页面
按需再生(On-demand Revalidation)
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';
export async function POST(request: Request) {
const { path, tag } = await request.json();
if (path) {
revalidatePath(path); // 重新生成指定路径
}
if (tag) {
revalidateTag(tag); // 重新生成指定标签的页面
}
return Response.json({ revalidated: true });
}
使用场景:
// CMS 更新文章时,调用 API 重新生成
await fetch('/api/revalidate', {
method: 'POST',
body: JSON.stringify({ path: '/blog/hello-world' }),
});
Next.js App Router 中的 ISR
方式 1:使用 next.revalidate
async function getPost(slug: string) {
const res = await fetch(`https://api.example.com/posts/${slug}`, {
next: { revalidate: 3600 }, // 每 1 小时重新生成
});
return res.json();
}
方式 2:使用 export const revalidate
// app/blog/page.tsx
export const revalidate = 3600; // 每 1 小时重新生成
async function getPosts() {
const res = await fetch('https://api.example.com/posts');
return res.json();
}
export default async function BlogPage() {
const posts = await getPosts();
return <div>{/* ... */}</div>;
}
方式 3:使用 Tags
// app/blog/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { tags: ['blog-posts'] }, // 添加标签
});
return res.json();
}
export default async function BlogPage() {
const posts = await getPosts();
return <div>{/* ... */}</div>;
}
按需再生:
import { revalidateTag } from 'next/cache';
// 当博客文章更新时
revalidateTag('blog-posts'); // 重新生成所有带 'blog-posts' 标签的页面
ISR 的优势
1. 性能接近 SSG
时间线:
缓存命中:50ms(和 SSG 一样快)
缓存未命中:返回缓存 + 后台生成(用户无感知)
2. 数据保持新鲜
对比:
| 渲染方式 | 数据新鲜度 |
|---|---|
| SSG | 构建时数据,可能过时 |
| ISR | 定时或按需更新,保持新鲜 |
| SSR | 实时数据,但性能差 |
3. 成本低
和 SSG 一样,大部分请求由 CDN 直接返回静态文件。
4. 可扩展性强
不需要重新构建整个网站,只重新生成变化的页面。
ISR 的劣势
1. 可能显示过时数据
问题:在重新生成完成前,用户看到的是旧数据。
示例:
T+0s 页面生成
T+59s 数据更新
T+60s 请求 → 返回旧数据(还未重新生成)
T+61s 后台重新生成
T+62s 请求 → 返回新数据
影响:用户可能看到 1-2 秒的过时数据。
2. 首次请求可能慢
问题:如果页面从未被访问,首次请求需要生成。
解决方案:
- 构建时预生成热门页面
- 使用
fallback: 'blocking'(Pages Router)
什么时候用 ISR?
| 场景 | 原因 |
|---|---|
| 电商商品页 | 价格、库存定期更新,需要 SEO |
| 新闻网站 | 内容定期更新,需要快速响应 |
| 博客 | 内容偶尔更新,需要 SEO 和性能 |
| 用户资料页 | 资料偶尔更新,页面数量多 |
4.5 RSC(React Server Components)内部机制
什么是 RSC
RSC(React Server Components):组件在服务端执行,不发送 JavaScript 到客户端。
与 SSR 的区别:
| 特性 | SSR | RSC |
|---|---|---|
| 服务端执行 | ✅ | ✅ |
| 生成 HTML | ✅ | ❌(生成 RSC Payload) |
| 发送 JS 到客户端 | ✅(Hydration) | ❌(零客户端 JS) |
| 可交互 | ✅(Hydration 后) | ❌(需要 Client Component) |
| 适用场景 | 需要交互的页面 | 展示型内容 |
RSC 的工作原理
步骤 1:服务端执行组件
// app/blog/page.tsx(Server Component)
async function getPosts() {
const res = await fetch('https://api.example.com/posts');
return res.json();
}
export default async function BlogPage() {
const posts = await getPosts();
return (
<div>
<h1>Blog</h1>
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
服务端执行:
- 执行
getPosts() - 渲染组件树
- 生成 RSC Payload(不是 HTML)
步骤 2:生成 RSC Payload
RSC Payload 示例(简化版):
{
"type": "div",
"props": {
"children": [
{
"type": "h1",
"props": { "children": "Blog" }
},
{
"type": "ul",
"props": {
"children": [
{ "type": "li", "props": { "children": "Post 1" } },
{ "type": "li", "props": { "children": "Post 2" } }
]
}
}
]
}
}
特点:
- 是组件树的序列化表示
- 不包含 JavaScript 代码
- 体积小(比 HTML 更小)
步骤 3:客户端渲染
浏览器接收 RSC Payload:
- 解析 RSC Payload
- 渲染为 DOM
- 无需 Hydration(因为没有 JS 代码)
时间线:
0ms 用户访问页面
100ms 服务端执行组件,生成 RSC Payload
150ms 返回 RSC Payload
200ms 浏览器渲染 DOM(完成)
对比 SSR:
SSR:
0ms 用户访问页面
100ms 服务端执行组件,生成 HTML
150ms 返回 HTML
200ms 浏览器显示 HTML
400ms 下载 JS Bundle
600ms Hydration 完成(可交互)
RSC:
0ms 用户访问页面
100ms 服务端执行组件,生成 RSC Payload
150ms 返回 RSC Payload
200ms 浏览器渲染 DOM(完成,无需 Hydration)
RSC 的优势
1. 零客户端 JavaScript
对比:
传统 React(CSR):
- React + ReactDOM: 130KB
- 组件代码: 50KB
- 总计: 180KB
SSR:
- React + ReactDOM: 130KB(Hydration 需要)
- 组件代码: 50KB(Hydration 需要)
- 总计: 180KB
RSC:
- React Runtime: 10KB(轻量级)
- 组件代码: 0KB(服务端执行)
- 总计: 10KB
结果:RSC 减少 95% 的客户端 JS。
2. 更快的首屏
原因:
- 无需下载大量 JS
- 无需 Hydration
- RSC Payload 体积小
3. 直接访问服务端资源
示例:直接读取数据库
// app/users/page.tsx(Server Component)
import { db } from '@/lib/db';
export default async function UsersPage() {
const users = await db.query('SELECT * FROM users');
return (
<div>
<h1>Users</h1>
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
优势:
- 无需 API 层
- 无需序列化/反序列化
- 更安全(数据库凭证不暴露到客户端)
4. 自动代码拆分
RSC 自动拆分:
// app/page.tsx
import HeavyChart from './HeavyChart'; // 500KB
export default function Page() {
return (
<div>
<h1>Dashboard</h1>
<HeavyChart />
</div>
);
}
如果 HeavyChart 是 Server Component:
- 500KB 代码在服务端执行
- 不发送到客户端
- 客户端只接收 RSC Payload
RSC 的局限性
1. 不可交互
问题:RSC 无法使用事件监听器、状态管理。
错误示例:
// ❌ Server Component 不能使用 useState
"use server"; // 默认就是 Server Component
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0); // ❌ 错误
return (
<button onClick={() => setCount(c => c + 1)}> // ❌ 错误
Count: {count}
</button>
);
}
解决方案:使用 Client Component
// ✅ Client Component
"use client";
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
);
}
2. 不能使用浏览器 API
问题:RSC 在服务端执行,无法访问 window、document、localStorage。
错误示例:
// ❌ Server Component 不能使用浏览器 API
export default function Page() {
const theme = localStorage.getItem('theme'); // ❌ 错误
return <div>Theme: {theme}</div>;
}
3. 不能使用某些 Hooks
问题:useState、useEffect、useContext 等只能在 Client Component 中使用。
可用 Hooks(Server Component):
use()(读取 Promise)useId()
RSC vs Client Component
| 特性 | Server Component | Client Component |
|---|---|---|
| 执行位置 | 服务端 | 客户端 |
| 发送 JS | ❌ 不发送 | ✅ 发送 |
| Hydration | ❌ 不需要 | ✅ 需要 |
| 事件监听 | ❌ 不支持 | ✅ 支持 |
| 状态管理 | ❌ 不支持 | ✅ 支持 |
| 浏览器 API | ❌ 不支持 | ✅ 支持 |
| 直接访问数据库 | ✅ 支持 | ❌ 不支持 |
| 适用场景 | 展示型内容 | 交互式 UI |
4.6 混合渲染实战:同一页面中 RSC + SSR + CSR 的协作
为什么需要混合渲染?
现实场景:一个页面通常包含多种内容:
- 静态内容:标题、描述(适合 SSG)
- 动态内容:用户信息、实时数据(适合 SSR)
- 交互内容:按钮、表单、图表(需要 Client Component)
- 展示内容:文章列表、商品列表(适合 RSC)
解决方案:混合使用多种渲染策略。
实战:电商商品详情页
页面结构:
商品详情页
├── Header(静态,SSG)
├── 商品信息(动态,RSC)
├── 价格/库存(实时,SSR)
├── 评论区(交互,Client Component)
└── 推荐商品(静态,RSC)
完整实现
// app/products/[id]/page.tsx
import ProductInfo from './ProductInfo';
import ProductPrice from './ProductPrice';
import ProductReviews from './ProductReviews';
import RecommendedProducts from './RecommendedProducts';
// 使用 ISR,每 10 分钟重新生成
export const revalidate = 600;
export default async function ProductPage({
params,
}: {
params: { id: string };
}) {
return (
<div>
{/* Header:静态内容,构建时生成 */}
<header className="bg-white shadow">
<h1>My Store</h1>
<nav>
<a href="/">Home</a>
<a href="/products">Products</a>
<a href="/cart">Cart</a>
</nav>
</header>
{/* 商品信息:RSC,服务端执行,零客户端 JS */}
<ProductInfo productId={params.id} />
{/* 价格/库存:SSR,每次请求动态获取 */}
<ProductPrice productId={params.id} />
{/* 评论区:Client Component,需要交互 */}
<ProductReviews productId={params.id} />
{/* 推荐商品:RSC,服务端执行 */}
<RecommendedProducts productId={params.id} />
</div>
);
}
// app/products/[id]/ProductInfo.tsx(Server Component)
import { db } from '@/lib/db';
export default async function ProductInfo({
productId,
}: {
productId: string;
}) {
// 直接访问数据库
const product = await db.query(
'SELECT * FROM products WHERE id = ?',
[productId]
);
return (
<div className="p-8">
<h1 className="text-3xl font-bold">{product.name}</h1>
<p className="text-gray-600 mt-4">{product.description}</p>
<img src={product.image} alt={product.name} />
</div>
);
}
// app/products/[id]/ProductPrice.tsx(Server Component,动态渲染)
import { db } from '@/lib/db';
export const dynamic = 'force-dynamic'; // 强制动态渲染
export default async function ProductPrice({
productId,
}: {
productId: string;
}) {
// 实时获取价格和库存
const price = await db.query(
'SELECT price, stock FROM inventory WHERE product_id = ?',
[productId]
);
return (
<div className="p-8 bg-yellow-50">
<p className="text-2xl font-bold text-red-600">
${price.price.toFixed(2)}
</p>
<p className="text-gray-600">
{price.stock > 0 ? `In Stock (${price.stock})` : 'Out of Stock'}
</p>
</div>
);
}
// app/products/[id]/ProductReviews.tsx(Client Component)
"use client";
import { useState } from 'react';
export default function ProductReviews({
productId,
}: {
productId: string;
}) {
const [reviews, setReviews] = useState([]);
const [newReview, setNewReview] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const res = await fetch(`/api/reviews`, {
method: 'POST',
body: JSON.stringify({ productId, content: newReview }),
});
const review = await res.json();
setReviews([...reviews, review]);
setNewReview('');
};
return (
<div className="p-8">
<h2 className="text-2xl font-bold mb-4">Reviews</h2>
<ul className="space-y-4">
{reviews.map(review => (
<li key={review.id} className="border p-4 rounded">
<p>{review.content}</p>
<p className="text-sm text-gray-500">
{new Date(review.createdAt).toLocaleDateString()}
</p>
</li>
))}
</ul>
<form onSubmit={handleSubmit} className="mt-8">
<textarea
value={newReview}
onChange={(e) => setNewReview(e.target.value)}
className="w-full border p-2 rounded"
placeholder="Write a review..."
/>
<button
type="submit"
className="mt-2 bg-blue-600 text-white px-4 py-2 rounded"
>
Submit
</button>
</form>
</div>
);
}
// app/products/[id]/RecommendedProducts.tsx(Server Component)
import { db } from '@/lib/db';
export default async function RecommendedProducts({
productId,
}: {
productId: string;
}) {
// 获取推荐商品
const recommendations = await db.query(`
SELECT * FROM products
WHERE category = (
SELECT category FROM products WHERE id = ?
)
AND id != ?
LIMIT 4
`, [productId, productId]);
return (
<div className="p-8 bg-gray-50">
<h2 className="text-2xl font-bold mb-4">You May Also Like</h2>
<div className="grid grid-cols-4 gap-4">
{recommendations.map(product => (
<a
key={product.id}
href={`/products/${product.id}`}
className="border p-4 rounded hover:shadow-lg"
>
<img src={product.image} alt={product.name} />
<h3 className="font-semibold mt-2">{product.name}</h3>
<p className="text-red-600">${product.price}</p>
</a>
))}
</div>
</div>
);
}
渲染策略分析
| 组件 | 渲染策略 | 原因 |
|---|---|---|
| Header | SSG(构建时生成) | 静态内容,所有用户看到一样 |
| ProductInfo | RSC(服务端执行) | 展示型内容,无需交互,直接访问数据库 |
| ProductPrice | SSR(动态渲染) | 实时价格和库存,每次请求都不同 |
| ProductReviews | Client Component | 需要交互(表单提交、状态管理) |
| RecommendedProducts | RSC(服务端执行) | 展示型内容,无需交互 |
性能分析
时间线:
0ms 用户访问页面
100ms CDN 返回缓存的 Header(SSG)
150ms 服务端执行 ProductInfo(RSC)
200ms 服务端执行 ProductPrice(SSR,实时查询)
250ms 流式发送 HTML 到浏览器
300ms 浏览器显示 Header、ProductInfo、ProductPrice
350ms 下载 ProductReviews 的 JS
400ms Hydration ProductReviews(可交互)
450ms 服务端执行 RecommendedProducts(RSC)
500ms 浏览器显示 RecommendedProducts
结果:
- 用户在 300ms 看到大部分内容
- 交互组件在 400ms 可交互
- 整体加载时间 < 500ms
对比纯 CSR:
CSR:
0ms 用户访问页面
500ms 下载 JS Bundle
1000ms 执行 JS
1500ms 请求 API(商品信息)
2000ms 请求 API(价格)
2500ms 请求 API(评论)
3000ms 请求 API(推荐)
3500ms 渲染完成
混合渲染比 CSR 快 7 倍。
混合渲染的最佳实践
1. 优先使用 Server Component
原则:如果组件不需要交互,使用 Server Component。
// ✅ 好的做法
export default function ProductList() {
// Server Component,直接访问数据库
const products = await db.query('SELECT * FROM products');
return <ul>{/* ... */}</ul>;
}
2. 将 Client Component 下沉到叶子节点
原则:尽量减少 Client Component 的范围。
// ❌ 不好的做法:整个页面都是 Client Component
"use client";
export default function Page() {
return (
<div>
<Header />
<ProductList />
<Button /> {/* 只有这个需要交互 */}
</div>
);
}
// ✅ 好的做法:只有 Button 是 Client Component
export default function Page() {
return (
<div>
<Header />
<ProductList />
<Button /> {/* "use client" */}
</div>
);
}
3. 使用 Suspense 分割加载状态
原则:慢速组件使用 Suspense,不阻塞整个页面。
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<Header />
<Suspense fallback={<div>Loading products...</div>}>
<ProductList />
</Suspense>
<Suspense fallback={<div>Loading recommendations...</div>}>
<RecommendedProducts />
</Suspense>
</div>
);
}
本章小结
渲染策略对比表
| 渲染策略 | 生成时机 | 性能 | SEO | 适用场景 | 劣势 |
|---|---|---|---|---|---|
| CSR | 浏览器运行时 | 差 | 差 | 内部系统、高度交互应用 | 白屏、SEO 差 |
| SSR | 每次请求 | 中 | 好 | 个性化内容、实时数据 | 服务器负载高 |
| SSG | 构建时 | 极好 | 好 | 博客、文档、营销页 | 数据可能过时 |
| ISR | 构建时 + 定时再生 | 好 | 好 | 电商、新闻、内容站 | 可能显示过时数据 |
| RSC | 服务端执行 | 极好 | 好 | 展示型内容、数据密集 | 不可交互 |
选择渲染策略的决策树
需要交互吗?
├─ 是 → 使用 Client Component
└─ 否 → 数据变化频率?
├─ 很少变化(几天/几周)→ SSG
├─ 定期变化(几分钟/几小时)→ ISR
├─ 每次请求都不同 → SSR 或 RSC(动态)
└─ 需要直接访问数据库?
├─ 是 → RSC
└─ 否 → SSR
Key Takeaways
- CSR 的局限性:白屏时间长、SEO 差、JS Bundle 大
- SSR 的优势:首屏快、SEO 好,但服务器负载高
- SSG 的优势:性能极佳、成本低,但数据可能过时
- ISR 的优势:结合 SSG 和 SSR,定时或按需更新
- RSC 的优势:零客户端 JS、直接访问数据库,但不可交互
- 混合渲染:同一页面中使用多种渲染策略,达到最佳性能
下一步
在下一章,我们将深入 React Server Components 的内部机制,理解 RSC vs 传统 SSR 的本质差异,以及 RSC 的多层缓存机制。
参考资料
- Next.js 官方文档:Rendering
- React 官方文档:Server Components
- Vercel Blog: How React Server Components Work
- Web.dev: Core Web Vitals
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。