渲染策略 — CodeRoadMap渲染策略
RSC/SSR/SSG/Streaming。
渲染策略
这一课你将能做到
- 说清楚 SSR、SSG、CSR、ISR、Server Components 五种渲染模式的区别和适用场景
- 在 Next.js 中用
force-dynamic、force-static、revalidate 控制页面的渲染行为
- 在 Server Component 和 Client Component 之间正确传递数据和事件
- 理解流式渲染(Streaming)的原理,并配合
<Suspense> 优化首屏体验
为什么要学
「这个页面是 SSR 还是 SSG?」是 Next.js 面试必问。但比名词更重要的是:同一个 Next.js 应用里,不同页面甚至同一页面的不同部分,可以同时使用不同的渲染策略。营销页用 SSG 做到极致缓存,仪表盘用 SSR 保证数据实时,评论区用 CSR 减少服务器压力,商品详情用 ISR 平衡 SEO 和更新频率——选型错了,要么性能差,要么服务器崩。
人话:渲染策略就像餐厅的出餐方式。SSG 是提前做好放保温柜,随取随走;SSR 是现点现做,新鲜但慢;ISR 是每 10 分钟重做一批,兼顾新鲜和效率;CSR 是给你食材自己炒,服务器最轻松但顾客等得久。
先建立图像
用户请求 → Next.js 服务器
│
┌───────────┼───────────┐
▼ ▼ ▼
静态缓存 服务端渲染 客户端渲染
(SSG/ISR) (SSR) (CSR)
│ │ │
▼ ▼ ▼
返回 HTML 返回 HTML 返回空壳 HTML
(含数据) (含数据) + JS bundle
│
▼
浏览器执行 JS
再请求数据渲染
关键区别:HTML 是在服务器生成的,还是在浏览器生成的?数据是在构建时获取的,还是请求时获取的,还是浏览器端获取的?
跟着做
步骤 1:SSG(静态生成)
默认情况下,没有动态数据获取的页面就是 SSG。app/page.tsx:
export default function Home() {
return (
<main>
<h1>纯静态页面</h1>
<p>构建时生成 HTML,之后每次访问都返回同一份缓存。</p>
</main>
);
}
构建验证:
npm run build
输出里找到 / 这一行,如果标记是 ○(Static),说明是 SSG。
步骤 2:SSR(服务端渲染)
在 app/ssr-demo/page.tsx 中,用 no-store 禁用缓存,强制每次请求都渲染:
export const dynamic = 'force-dynamic';
async function getTime() {
const res = await fetch('https://worldtimeapi.org/api/timezone/Asia/Shanghai', {
cache: 'no-store',
});
return res.json();
}
export default async function SSRPage() {
const data = await getTime();
return (
<main>
<h1>SSR 页面</h1>
<p>服务器时间:{data.datetime}</p>
<p>每次刷新都会重新请求 API。</p>
</main>
);
}
构建时这个页面标记为 ƒ(Dynamic),每次访问都执行 getTime()。
步骤 3:ISR(增量静态再生)
:
app/isr-demo/page.tsx
export const revalidate = 60; // 60 秒
async function getTime() {
const res = await fetch('https://worldtimeapi.org/api/timezone/Asia/Shanghai', {
next: { revalidate: 60 },
});
return res.json();
}
export default async function ISRPage() {
const data = await getTime();
return (
<main>
<h1>ISR 页面</h1>
<p>服务器时间:{data.datetime}</p>
<p>构建时生成,之后每 60 秒重新生成一次。</p>
</main>
);
}
第一次访问返回缓存的 HTML,同时后台触发重新生成;60 秒内的访问都返回旧缓存,60 秒后第一个访问者触发新缓存生成。
步骤 4:CSR(客户端渲染)
'use client';
import { useEffect, useState } from 'react';
export default function CSRPage() {
const [time, setTime] = useState('');
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('https://worldtimeapi.org/api/timezone/Asia/Shanghai')
.then(r => r.json())
.then(data => {
setTime(data.datetime);
setLoading(false);
});
}, []);
return (
<main>
<h1>CSR 页面</h1>
{loading ? <p>加载中…</p> : <p>浏览器时间:{time}</p>}
<p>HTML 是空壳,数据在浏览器里请求。</p>
</main>
);
}
查看页面源代码,HTML 里没有时间数据——数据在浏览器执行 JS 后才出现。
步骤 5:Server Component 与 Client Component 协作
app/mixed/page.tsx(Server Component):
import ClientButton from './ClientButton';
async function getPosts() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=5');
return res.json();
}
export default async function MixedPage() {
const posts = await getPosts();
return (
<main>
<h1>Server + Client 混合</h1>
<ul>
{posts.map((p: any) => (
<li key={p.id}>
{p.title}
<ClientButton postId={p.id} />
</li>
))}
</ul>
</main>
);
}
app/mixed/ClientButton.tsx(Client Component):
'use client';
import { useState } from 'react';
export default function ClientButton({ postId }: { postId: number }) {
const [liked, setLiked] = useState(false);
return (
<button
onClick={() => setLiked(!liked)}
style={{ marginLeft: 8, color: liked ? 'red' : 'gray' }}
>
{liked ? '❤️' : '🤍'} {postId}
</button>
);
}
Server Component 负责取数据和渲染列表,Client Component 只负责点赞按钮的交互。按钮的 JS 会被打包到客户端,但列表的 HTML 在服务器生成。
步骤 6:流式渲染(Streaming)
import { Suspense } from 'react';
import SlowList from './SlowList';
import FastHeader from './FastHeader';
export default function StreamingPage() {
return (
<main>
<FastHeader />
<Suspense fallback={<p>列表加载中…</p>}>
<SlowList />
</Suspense>
</main>
);
}
app/streaming/FastHeader.tsx:
export default function FastHeader() {
return <h1>标题立刻出现</h1>;
}
app/streaming/SlowList.tsx:
async function getData() {
await new Promise(r => setTimeout(r, 3000));
return ['条目 A', '条目 B', '条目 C'];
}
export default async function SlowList() {
const items = await getData();
return (
<ul>
{items.map(i => <li key={i}>{i}</li>)}
</ul>
);
}
访问页面:标题立刻渲染,3 秒后列表才出现。Next.js 把 HTML 分块传输,先传快的部分,慢的部分通过 Suspense 边界流式补上。
讲清楚要点
- SSG(Static Site Generation):构建时生成 HTML,之后所有请求返回同一份文件。适合内容不变的页面:营销页、文档、博客文章。用
next build 时生成,运行时零计算。
- SSR(Server-Side Rendering):每次请求都在服务器执行组件并生成 HTML。适合数据实时性要求高的页面:仪表盘、订单页。用
export const dynamic = 'force-dynamic' 或 cache: 'no-store' 触发。
- ISR(Incremental Static Regeneration):SSG + 定时刷新。构建时生成,之后按
revalidate 秒数重新生成。适合内容偶尔更新的页面:商品详情、新闻列表。
- CSR(Client-Side Rendering):HTML 只有空壳,数据在浏览器请求后渲染。适合 SEO 不重要的交互页面:后台管理、聊天室。用
'use client' + useEffect 实现。
- Server Component:默认在服务端执行,不打包到客户端 bundle,可以直接访问数据库、文件系统。不能用 hooks、事件、浏览器 API。
- Client Component:
'use client' 标记,打包到客户端,可以用 hooks、事件、浏览器 API。尽量把 Client Component 放在组件树的叶子节点,减少 bundle 体积。
- Streaming:React 18 的 Suspense 让 HTML 可以分块传输。快的部分先渲染,慢的部分后补。Next.js 自动对 async Server Component 启用流式传输,配合
loading.tsx 或 <Suspense> 控制兜底 UI。
你可能会懵的地方
-
Server Component 里能写 console.log 吗?
能,但打印在服务器终端,不在浏览器控制台。调试 Server Component 要看终端输出。
-
Server Component 能直接用 fetch 吗?
能,而且 Next.js 对 Server Component 里的 fetch 做了扩展:自动缓存、自动去重、支持 next: { revalidate, tags } 选项。
-
Client Component 里能 import Server Component 吗?
不能。Client Component 的 import 链上所有组件都会被标记为客户端。正确的做法是 Server Component 作为父组件,把 Client Component 作为 children 传入。
-
force-dynamic 和 cache: 'no-store' 有什么区别?
force-dynamic 强制整个路由为 SSR,包括所有 fetch;cache: 'no-store' 只禁用当前 fetch 的缓存,其他 fetch 可能仍有缓存。force-dynamic 是更粗粒度的开关。
-
ISR 的 revalidate 是精确的吗?
不是。revalidate = 60 表示「最多 60 秒刷新一次」,不是「每 60 秒准时刷新」。流量低的页面可能很久才触发一次重新生成。
-
Streaming 会影响 SEO 吗?
不会。搜索引擎爬虫会等整个 HTML 传输完成再解析,流式渲染只是把传输过程分块,最终 HTML 是完整的。
错法对照
| 错法 | 问题 | 正解 |
|---|
Server Component 里写 useState | 编译报错 | 拆出 Client Component 或整文件加 'use client' |
Client Component 里 await fetch | 客户端组件不支持 async 组件 | 用 useEffect + useState 或 SWR |
以为 revalidate 是定时器 | 它是「最大缓存时间」,不是 cron | 低流量页面考虑用 revalidatePath 主动刷新 |
在 Server Component 里用 window | 服务器没有 window,报错 | 移到 Client Component 或 useEffect |
给所有页面加 force-dynamic | 失去缓存,服务器压力大 | 只在真正需要实时数据的页面用 |
| 把 Client Component 放在组件树顶层 | 整棵树都打包到客户端 | 把交互逻辑下沉到叶子节点 |
卡点急救
- 构建时 SSG 页面报错:SSG 页面在构建时执行,如果 fetch 失败或环境变量缺失,构建会失败。给 fetch 加 try/catch,或用
dynamic = 'force-dynamic' 改为 SSR。
- ISR 页面数据不更新:检查是否多个 fetch 用了不同 revalidate,以最短的为准。也检查 CDN 缓存——Vercel 的 CDN 可能缓存了旧响应。
- hydration mismatch:Server 和 Client 渲染结果不一致。常见原因:
new Date()、Math.random()、window 直接出现在组件顶层。用 suppressHydrationWarning 或移到 useEffect。
- Server Component 里 import 了客户端库:比如
import { useState } from 'react' 或 import { motion } from 'framer-motion'(默认是客户端)。确认库是否支持 RSC,不支持就拆到 Client Component。
- 流式渲染不生效:确保慢组件是 async Server Component,且被
<Suspense> 包裹。如果整个页面都是同步渲染,没有 Suspense 边界,就不会流式传输。
面试常问
-
SSR 和 SSG 的区别?各自适用场景?
SSR 每次请求在服务器渲染,数据实时但服务器有计算开销;SSG 构建时生成,之后零计算但数据可能过期。SSR 适合实时数据(仪表盘、订单),SSG 适合静态内容(营销页、文档)。
-
ISR 是怎么工作的?
构建时生成静态 HTML,同时记录 revalidate 时间。第一个请求返回缓存,同时后台异步重新生成。后续请求在缓存过期前返回旧缓存,过期后返回新缓存。实现了「静态的速度 + 动态的更新」。
-
Server Component 和 Client Component 的边界怎么划?
按交互性划分:纯展示、数据获取、SEO 关键的用 Server Component;有状态、事件、浏览器 API 的用 Client Component。尽量把 Client Component 放在叶子节点,减少打包体积。
-
Next.js 的渲染策略可以混用吗?
可以,而且推荐。同一个应用里,首页 SSG、商品详情 ISR、用户中心 SSR、评论区 CSR,各取所长。
-
什么是 React Server Components?
React 官方提出的组件类型,只在服务端执行,渲染结果是序列化的 RSC payload(不是 HTML),客户端 React 可以直接渲染。优势:零 bundle、直读后端、自动代码分割。Next.js App Router 是 RSC 的主要实现。
自测三问
- 一个页面用
export const revalidate = 3600,第一个访问者看到旧数据,第二个访问者(1 小时后)看到新数据,中间发生了什么?
- Server Component 里为什么不能写
onClick?如果一定要交互怎么办?
force-dynamic 和 revalidate = 0 效果一样吗?为什么?
小结一句话
Next.js 的渲染策略不是单选题,而是组合题:SSG 管静态、SSR 管实时、ISR 管平衡、CSR 管交互、Streaming 管体验——按页面甚至按组件粒度选型,才能既快又省。
延伸