This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
2026-04-29 23:36:32 +08:00

430 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [React, Next.js, SSR, SSG, ISR, Frontend, Server Components]
create time: 2026-04-29 22:13
---
# Server-Side Rendering
## 概述
服务端渲染(SSR)让 React 组件在服务器端预渲染为 HTML,显著改善首屏加载速度和 SEO。本文档以 Next.js App Router 为核心,介绍 SSR/SSG/ISR 的渲染策略、Server Components 体系与最佳实践。
> [!question] 思考:用户从输入 URL 到看到页面,中间经历了哪些步骤?
>
> 传统 CSR 模式下,浏览器先拿到一个几乎空的 HTML,然后下载 JS bundle,执行 React 来生成内容——用户需要等待两件事都完成才能看到页面。SSR 把「生成 HTML」这一步搬到服务器做,用户请求回来时就已经有可读的内容了。
## 渲染模式对比
```mermaid
graph TB
subgraph CSR["客户端渲染 CSR"]
A["HTML空白页"] --> B["下载JS Bundle"]
B --> C["执行React hydration"]
C --> D["显示内容"]
end
subgraph SSR["服务端渲染 SSR"]
E["请求页面"] --> F["服务器渲染React -> HTML"]
F --> G["发送含内容的HTML"]
G --> H["客户端hydration"]
H --> I["交互可用"]
end
subgraph SSG["静态生成 SSG"]
J["构建时渲染"] --> K["生成纯HTML文件"]
K --> L["CDN分发"]
end
style F fill:#4FC08D,color:#fff
style H fill:#F5A87D,color:#000
style J fill:#61DAFB,color:#000
```
### 三种策略决策表
| 策略 | 适用场景 | 数据时效性 | 构建参与 |
|------|----------|-----------|---------|
| **SSR**(Server Render) | 个性化页面、实时数据 | ✅ 每次请求实时生成 | ❌ |
| **SSG**(Static Site Generation) | 博客、文档、营销页 | ⏱️ 构建时生成 | ✅ |
| **ISR**(Incremental Static Regeneration) | 新闻列表、商品目录 | 🔄 定时后台更新 | ✅(增量) |
> [!tip] 核心区别一句话
>
> - **SSR**:每个用户请求都触发一次服务端的完整渲染。
> - **SSG**:构建时生成一次 HTML,所有用户共享同一个静态页面。
> - **ISR**:先用 SSG 生成的静态页面响应,后台静默重新生成后替换——对用户无感知。
## Next.js App Router 架构
> [!note] Server Component 是默认值
>
> Next.js App Router 中,所有组件默认就是 **Server Component**——除非你在文件顶部声明 `"use client"`。这意味着你可以放心地在组件里 await 数据库查询、读取环境变量或访问文件系统,这些代码永远不会发送到浏览器。
```tsx
// app/layout.tsx —— 根布局(所有页面共享)
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh">
<body>
<Navbar />
{children}
</body>
</html>
);
}
// app/page.tsx —— 首页(SSR by default)
async function HomePage() {
// ✅ 直接在组件中 await API
const posts = await fetchPosts();
return (
<main>
{posts.map(post => <PostCard key={post.id} post={post} />)}
</main>
);
}
// app/blog/[slug]/page.tsx —— 动态路由页
async function PostPage({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug);
return <article>{post.content}</article>;
}
```
## Streaming SSR + Suspense
```mermaid
sequenceDiagram
participant Client as 浏览器
participant Server as 服务器
Client->>Server: GET /dashboard
Server->>Server: 并行请求 user/profile/orders
Note over Server: 每个请求可有自己的Suspense boundary
Server-->>Client: HTML: Navbar + Sidebar<br/>(立即显示,约200ms)
Server-->>Client: Stream: Profile card<br/>(中等优先级,约800ms)
Server-->>Client: Stream: Order history<br/>(低优先级,约1500ms)
Note over Client: 用户体验:渐进式展示,而非等全部完成
```
```tsx
// Dashboard layout(流式渲染的关键)
function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<>
{/* 非关键 UI 用 Suspense 包裹 */}
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
{children}
{/* 各区域独立 Suspense */}
<Suspense fallback={<StatsSkeleton />}>
<StatsPanel />
</Suspense>
<Suspense fallback={<OrdersSkeleton />}>
<OrdersPanel />
</Suspense>
</>
);
}
```
> [!note] Streaming SSR 的工作原理
>
> 服务端将 HTML 分成多个 chunk,通过网络流逐步发送给浏览器。浏览器边收边渲染——不需要等所有数据就绪。**优先级高的区域先出,低的在后**,用户体验从「全部等」变成「渐进可见」。
## Data Fetching 策略
```tsx
// 方案1:直接 await(默认缓存 + 共享缓存)
async function Page() {
const data = await fetchData(); // 自动缓存,同路径请求去重
return <div>{data}</div>;
}
// 方案2:带 revalidate 的 ISR
async function Page() {
const data = await fetchData({ next: { revalidate: 60 } }); // 60秒后后台重新验证
return <div>{data}</div>;
}
// 方案3:no-store(强制 SSR,不走缓存)
async function Page() {
const data = await fetchData({ cache: "no-store" }); // 每次请求都获取最新数据
return <div>{data}</div>;
}
// 方案4:client component 中的 fetch(使用 TanStack Query)
"use client";
function Page() {
const { data } = useQuery({ queryKey: ["data"], queryFn: () => fetch("/api/data").then(r => r.json()) });
return <div>{data}</div>;
}
```
### Cache vs Revalidate vs no-store 决策指南
| 策略 | 行为 | 适合场景 |
|------|------|---------|
| `fetch(url)` 不加配置 | 基于 HTTP 协议的永久缓存(直到下次部署失效) | 不常变化的配置数据 |
| `{ next: { revalidate: N } }` | CDN 级别缓存,N 秒后后台重新验证 | 商品信息、文章列表等 |
| `{ cache: "no-store" }` | 完全不走缓存,每次都回源 | 用户面板、实时仪表盘 |
| `{ next: { tags: ["posts"] } }` + `revalidateTag("posts")` | 按标签精确失效 | CMS 系统发布新文章时触发更新 |
## Server Component vs Client Component 边界
```tsx
// server component(默认,无需声明)
async function ServerComponent() {
// ✅ 可以直接访问数据库、API密钥、文件系统
const db = await dbConnection.query("SELECT * FROM users");
return <UsersList users={db} />;
}
// client component(需显式声明)
"use client";
function InteractiveChart() {
// ✅ 可以使用 useState/useEffect/DOM API
const [zoom, setZoom] = useState(1);
useEffect(() => { ... }, []);
return <canvas ref={canvasRef} />;
}
// Parent
function Dashboard() {
return (
<>
<ServerComponent /> {/* 在服务端渲染 */}
<InteractiveChart /> {/* 在客户端渲染 */}
</>
);
}
```
> [!warning] Client → Server 通信限制
>
> - Client Component 无法直接调用 Server Component 的方法或 props
> - Server Component 也不能传给 Client Component 引用了函数、Promise 或 Generator 的值
> - 解决方案:通过 URL 参数、cookies、或后端 API 传递数据
### 何时该用 Server Component?何时该用 Client Component?
> [!question] 如何判断一个组件应该放在哪一边?
>
> 记住一条黄金法则:**能放服务器的就放服务器**。Server Component 是零 bundle size 的——它们不会增加客户端 JavaScript 体积。只有当你确实需要浏览器专属 API(事件监听、状态管理、DOM 操作)时才降级到 Client Component。
| 需要浏览器能力吗? | 推荐方案 |
|-------------------|---------|
| ❌ 不需要(只渲染数据) | Server Component ✅ |
| ✅ 需要 `useState` / `useEffect` / `onClick` | Client Component (`"use client"`) |
| ✅ 需要第三方交互库(地图、图表) | Client Component |
| ✅ 需要浏览器 API(localStorage、Geolocation) | Client Component |
| ❌ 只需要从 API 获取数据并展示 | Server Component ✅ |
## Server Actions
Server Action 允许你在服务端定义可直接从 Client Component 调用的异步函数——无需手动写 API 路由。
```tsx
// app/actions.ts —— 独立的 Server Action 模块
"use server";
import { revalidatePath } from "next/cache";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
const content = formData.get("content") as string;
// 数据库写入
await db.post.create({ data: { title, content } });
// 成功后刷新对应页面的缓存
revalidatePath("/blog");
}
```
```tsx
// app/blog/new/page.tsx —— 表单页面(Client Component)
"use client";
import { createPost } from "@/app/actions";
function NewPostForm() {
async function handleSubmit(formData: FormData) {
await createPost(formData);
// 提交后跳转到列表页
router.push("/blog");
}
return (
<form action={handleSubmit}>
<input name="title" placeholder="标题" required />
<textarea name="content" placeholder="内容" required />
<button type="submit">发布</button>
</form>
);
}
```
> [!tip] Server Actions 的优势
>
> 1. **零样板代码**:不再需要手动编写 API Route + fetch 调用链
> 2. **类型安全**:函数签名天然携带类型信息
> 3. **内置序列化和校验**:FormData 自动解析,配合 Zod 做 schema 校验
> 4. **直接操作服务端状态**:数据库读写、文件操作、认证上下文一步到位
## Metadata API & 动态 SEO
Next.js 提供了声明式的元数据 API,自动生成 `<head>` 中的标签。
```tsx
// app/blog/[slug]/page.tsx —— 动态元数据
import { getPostBySlug } from "@/lib/posts";
// 静态 generateMetadata(构建时可确定)
export async function generateMetadata({ params }) {
const post = await getPostBySlug(params.slug);
return {
title: `${post.title} | My Blog`,
description: post.excerpt,
openGraph: { title, images: [post.coverImage] },
};
}
// 动态 generateViewport / robots / sitemap
export function generateStaticParams() {
const posts = getAllPosts();
return posts.map(post => ({ slug: post.slug }));
}
```
> [!note] generateStaticParams vs dynamicParams
>
> 如果未列出的动态路由被访问到,Next.js 默认会返回 404。你可以通过 `next.config.js` 设置 `dangerouslyAllowHostnameMismatch = true` 或在路由目录中添加 `not-found.tsx` 来自定义 404 行为。对于 API 驱动的项目,可以设置 `dynamicParams = false` 确保安全性。
## Cookies、Headers 与 Auth
Server Component 可以直接读取和修改 cookies/headers,这是构建认证系统的基石。
```tsx
// app/dashboard/page.tsx —— Server Component
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
async function DashboardPage() {
// 读取 cookie
const sessionCookie = (await cookies()).get("session");
// 验证 token
if (!sessionCookie) {
redirect("/login");
}
// 读取请求头
const userAgent = (await headers()).get("user-agent");
// 使用 session 数据渲染页面
return <DashboardContent session={sessionCookie.value} />;
}
```
## 错误处理
SSR 环境下的错误处理需要考虑服务端和网络异常的双重场景。
```tsx
// app/error.tsx —— 捕获渲染阶段的错误
"use client";
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
return (
<div>
<h2>出错了</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>重试</button>
</div>
);
}
// app/not-found.tsx —— 自定义 404 页面
export default function NotFound() {
return <h2>页面不存在</h2>;
}
// 异步组件中的优雅降级
async function CommentsSection({ postId }: { postId: string }) {
// 评论组件失败不影响整个页面
let comments;
try {
comments = await fetchComments(postId);
} catch {
comments = []; // 降级为空数组
}
return <ul>{comments.map(c => <li key={c.id}>{c.text}</li>)}</ul>;
}
```
## 性能调优
### 关键指标与优化方向
```mermaid
graph LR
A["TTFB<br/>Time to First Byte"] -->|"降低服务器处理时间"| B["边缘缓存<br/>ISR / CDN"]
C["LCP<br/>Largest Contentful Paint"] -->|"减小首屏 JS 体积"| D["Server Components ✅"]
E["CLS<br/>Cumulative Layout Shift"] -->|"预留空间防抖动"| F["固定尺寸 + Aspect Ratio"]
style A fill:#F5A87D,color:#000
style C fill:#F5A87D,color:#000
style E fill:#F5A87D,color:#000
```
### Image Optimization & Font Optimization
```tsx
import Image from "next/image";
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"] }); // 零 CLS 字体加载
// next/image 自动做 WebP 转换 + responsive srcset + lazy loading
<Image
src="/hero.jpg"
alt="Hero image"
width={1200}
height={600}
placeholder="blur" // 懒加载时用模糊缩略图占位
priority // 首屏图片提前加载
/>
```
## 最佳实践
> [!tip] 实战经验总结
### 要点清单
1. **Server Component 优先**:默认情况下所有组件都是 Server Component,充分利用其零 bundle size 和数据直取的能力。只有在需要交互时才添加 `"use client"`。
2. **Suspense 粒度要合理**:不要把所有东西包在一个大 Suspense 里(起不到流式效果),也不要每个小组件都加(overhead)。按数据依赖层级划分边界。
3. **合理使用数据缓存策略**:`revalidate` 适合大多数内容型数据,`no-store` 用于用户相关实时数据,`tags` 用于精确实时失效控制。
4. **Server Actions 替代手写 API**:减少样板代码的同时保持类型安全,但注意不要滥用——简单 CRUD 以外仍建议走标准的 API Route 模式。
5. **做好错误降级**:SSR 中一个组件出错会导致整页白屏,对非关键组件要用 try-catch 兜底或 `<Suspense>` 隔离。
6. **善用 Metadata API**:SEO 相关的工作都应该在 `generateMetadata` 等 API 中完成,避免在 JSX 中手动操作 head。
7. **关注 Core Web Vitals**:Server Components 天然利好 LCP(减少 JS),ISR + CDN 利好 TTFB,Image/Font Optimization 利好 CLS。
## 关联笔记
- [[13-并发特性]]
- [[15-性能优化]]
- [[09-状态管理]]