第 4 章:现代 Web 渲染模型全景

深入理解现代 Web 渲染策略:CSR、SSR、SSG、ISR、RSC 的原理、优缺点和适用场景,掌握如何在 Next.js 中选择合适的渲染模式,实现最佳的性能和用户体验。

本章是卷 II 的开篇章节。读完本章,你将建立完整的渲染模型心智地图,理解每种渲染策略的工作原理、优缺点和适用场景。


4.1 CSR 的局限性

什么是 CSR(Client-Side Rendering)

CSR(客户端渲染) 是传统 React SPA 的渲染方式:

  1. 浏览器下载空的 HTML 骨架
  2. 下载并执行 JavaScript
  3. React 在浏览器中渲染 UI
  4. 发起 API 请求获取数据
  5. 更新 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. 首屏快

时间线对比:

阶段CSRSSR
下载 HTML50ms(空 HTML)200ms(完整 HTML)
显示内容2500ms(等待 JS + 数据)300ms(立即显示)
可交互2500ms800ms(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

指标CSRSSR
LCP3-5s ❌0.5-1s ✅
FCP2-3s ❌0.3-0.5s ✅
TTI4-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 执行:

  1. 渲染每个页面
  2. 生成 HTML 文件
  3. 保存到 .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>
  );
}

行为:

  1. 首次请求:返回构建时生成的页面
  2. 60 秒内的请求:返回缓存页面(快速)
  3. 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 的区别:

特性SSRRSC
服务端执行✅✅
生成 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>
  );
}

服务端执行:

  1. 执行 getPosts()
  2. 渲染组件树
  3. 生成 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:

  1. 解析 RSC Payload
  2. 渲染为 DOM
  3. 无需 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 ComponentClient 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>
  );
}

渲染策略分析

组件渲染策略原因
HeaderSSG(构建时生成)静态内容,所有用户看到一样
ProductInfoRSC(服务端执行)展示型内容,无需交互,直接访问数据库
ProductPriceSSR(动态渲染)实时价格和库存,每次请求都不同
ProductReviewsClient Component需要交互(表单提交、状态管理)
RecommendedProductsRSC(服务端执行)展示型内容,无需交互

性能分析

时间线:

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

  1. CSR 的局限性:白屏时间长、SEO 差、JS Bundle 大
  2. SSR 的优势:首屏快、SEO 好,但服务器负载高
  3. SSG 的优势:性能极佳、成本低,但数据可能过时
  4. ISR 的优势:结合 SSG 和 SSR,定时或按需更新
  5. RSC 的优势:零客户端 JS、直接访问数据库,但不可交互
  6. 混合渲染:同一页面中使用多种渲染策略,达到最佳性能

下一步

在下一章,我们将深入 React Server Components 的内部机制,理解 RSC vs 传统 SSR 的本质差异,以及 RSC 的多层缓存机制。


参考资料

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「frontend」更多文章

  1. Vue 性能优化指南:虚拟列表、懒加载、渲染优化与 Core Web Vitals
  2. Nuxt.js 完全指南:Vue 全栈框架的 SSR、SSG、API 路由与部署实践
  3. Vue 测试深度指南:Vitest + Vue Test Utils + Playwright E2E 与 CI 集成