--- 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 ( {children} ); } // app/page.tsx —— 首页(SSR by default) async function HomePage() { // ✅ 直接在组件中 await API const posts = await fetchPosts(); return (
{posts.map(post => )}
); } // app/blog/[slug]/page.tsx —— 动态路由页 async function PostPage({ params }: { params: { slug: string } }) { const post = await getPostBySlug(params.slug); return
{post.content}
; } ``` ## 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
(立即显示,约200ms) Server-->>Client: Stream: Profile card
(中等优先级,约800ms) Server-->>Client: Stream: Order history
(低优先级,约1500ms) Note over Client: 用户体验:渐进式展示,而非等全部完成 ``` ```tsx // Dashboard layout(流式渲染的关键) function DashboardLayout({ children }: { children: React.ReactNode }) { return ( <> {/* 非关键 UI 用 Suspense 包裹 */} }> {children} {/* 各区域独立 Suspense */} }> }> ); } ``` > [!note] Streaming SSR 的工作原理 > > 服务端将 HTML 分成多个 chunk,通过网络流逐步发送给浏览器。浏览器边收边渲染——不需要等所有数据就绪。**优先级高的区域先出,低的在后**,用户体验从「全部等」变成「渐进可见」。 ## Data Fetching 策略 ```tsx // 方案1:直接 await(默认缓存 + 共享缓存) async function Page() { const data = await fetchData(); // 自动缓存,同路径请求去重 return
{data}
; } // 方案2:带 revalidate 的 ISR async function Page() { const data = await fetchData({ next: { revalidate: 60 } }); // 60秒后后台重新验证 return
{data}
; } // 方案3:no-store(强制 SSR,不走缓存) async function Page() { const data = await fetchData({ cache: "no-store" }); // 每次请求都获取最新数据 return
{data}
; } // 方案4:client component 中的 fetch(使用 TanStack Query) "use client"; function Page() { const { data } = useQuery({ queryKey: ["data"], queryFn: () => fetch("/api/data").then(r => r.json()) }); return
{data}
; } ``` ### 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 ; } // client component(需显式声明) "use client"; function InteractiveChart() { // ✅ 可以使用 useState/useEffect/DOM API const [zoom, setZoom] = useState(1); useEffect(() => { ... }, []); return ; } // Parent function Dashboard() { return ( <> {/* 在服务端渲染 */} {/* 在客户端渲染 */} ); } ``` > [!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 (