191 lines
5.1 KiB
Markdown
191 lines
5.1 KiB
Markdown
|
|
---
|
|||
|
|
tags: [React, Next.js, SSR, SSG, ISR, Frontend]
|
|||
|
|
create time: 2026-04-29 22:13
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# Server-Side Rendering
|
|||
|
|
|
|||
|
|
## 概述
|
|||
|
|
|
|||
|
|
服务端渲染(SSR)让 React 组件在服务器端预渲染为 HTML,显著改善首屏加载速度和 SEO。本文档以 Next.js App Router 为核心,介绍 SSR/SSG/ISR 的渲染策略与最佳实践。
|
|||
|
|
|
|||
|
|
## 渲染模式对比
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
graph TB
|
|||
|
|
subgraph "客户端渲染 CSR"
|
|||
|
|
A[HTML空白页] --> B["下载 JS Bundle"]
|
|||
|
|
B --> C["执行 React hydration"]
|
|||
|
|
C --> D["显示内容"]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
subgraph "服务端渲染 SSR"
|
|||
|
|
E[请求页面] --> F["服务器渲染 React → HTML"]
|
|||
|
|
F --> G["发送含内容的 HTML"]
|
|||
|
|
G --> H["客户端 hydration"]
|
|||
|
|
H --> I["交互可用"]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
subgraph "静态生成 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) | 新闻列表、商品目录 | 🔄 定时后台更新 | ✅(增量) |
|
|||
|
|
|
|||
|
|
## Next.js App Router 架构
|
|||
|
|
|
|||
|
|
```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>
|
|||
|
|
</>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 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>;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## SSR 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
|
|||
|
|
> - 解决方案:通过 URL 参数、cookies、或后端 API 传递数据
|
|||
|
|
|
|||
|
|
## 关联笔记
|