引言
Next.js 作为 React 生態中最成熟的全端框架,提供了多种渲染策略来满足不同场景的需求。但正因为选择太多,很多开发者在实际项目中常常困惑:什么时候用 SSR?什么时候用 SSG?ISR 又适合什么场景?这篇文章将带你深入理解这些渲染模式,并分享真实项目中的踩坑经验。
Next.js 渲染模式全解析
1. 静态站点生成 (SSG)
原理:在构建时生成 HTML,每次请求复用相同的页面。
适用场景:
- 博客、文档网站
- 产品介绍页
- 不频繁更新的内容页面
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
export default async function BlogPost({ params }) {
const post = await getPost(params.slug);
return <article>{post.content}</article>;
}
优点:
- 极速加载(CDN 缓存)
- SEO 友好
- 降低服务器成本
缺点:
- 构建时确定,适合静态内容
- 更新需要重新部署
2. 服务器组件 (RSC)
原理:组件在服务器端渲染,默认不发送 JavaScript 到客户端。
适用场景:
- 需要直接访问数据库或文件系统的组件
- 大型数据获取逻辑
- 减少客户端 bundle 大小
// app/dashboard/page.tsx
async function getData() {
const res = await fetch('https://api.example.com/data', {
cache: 'no-store', // 动态获取
});
return res.json();
}
export default async function Dashboard() {
const data = await getData();
return <div>{data.content}</div>;
}
优点:
- 零客户端 JavaScript
- 直接访问后端资源
- 自动代码分割
缺点:
- 交互组件需要转为客户端组件
- 学习曲线(需要理解服务器/客户端边界)
3. 动态渲染 (SSR)
原理:每次请求时实时渲染页面。
适用场景:
- 个性化内容(用户专属页面)
- 实时数据展示
- 需要请求参数的页面
// app/user-profile/page.tsx
import { headers } from 'next/headers';
export default async function ProfilePage() {
const headersList = headers();
const cookie = headersList.get('cookie');
// 每次请求都获取最新数据
const user = await getUserFromCookie(cookie);
return <div>Welcome, {user.name}!</div>;
}
优点:
- 实时数据
- SEO 友好
- 个性化内容
缺点:
- TTFB(首字节时间)较慢
- 高并发时服务器压力大
4. 增量静态再生成 (ISR)
原理:构建时生成静态页面,后台定期重新验证更新。
适用场景:
- 频繁更新的内容(博客、新闻)
- 电商产品页
- 需要 SEO 且内容会更新的页面
// app/products/[id]/page.tsx
export default async function ProductPage({ params }) {
const product = await getProduct(params.id);
return (
<div>
<h1>{product.name}</h1>
<p>{product.price}</p>
</div>
);
}
// 每60秒重新验证
export const revalidate = 60;
优点:
- 静态速度 + 动态更新
- 无需重新部署
- 适合大多数内容型网站
缺点:
- 更新有延迟(最大一个 revalidate 周期)
- 需要理解缓存机制
渲染模式选择决策树
├── 数据是否需要实时?
│ ├── 是 → SSR (动态渲染)
│ └── 否 → 下一题
├── 内容更新频率?
│ ├── 极高(每分钟) → SSR
│ ├── 较高(每天) → ISR
│ └── 很低(几个月) → SSG
└── 是否需要用户个性化?
├── 是 → SSR 或客户端获取
└── 否 → SSG / ISR
实战踩坑与解决方案
踩坑 1:客户端组件 vs 服务器组件边界混淆
问题:在服务器组件中直接使用 useState 或 onClick,导致报错。
// ❌ 错误:服务器组件中使用交互 API
export default function Page() {
const [count, setCount] = useState(0); // 报错!
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
解决:使用 'use client' 声明客户端组件,或将交互逻辑拆分。
// ✅ 方案一:拆分为客户端组件
'use client';
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
// ✅ 方案二:服务器组件包裹客户端组件
export default function Page() {
return (
<div>
<h1>My Page</h1>
<Counter />
</div>
);
}
踩坑 2:fetch 缓存行为理解错误
问题:以为设置了 revalidate 就会实时更新,但实际上默认行为不同。
// ❌ 错误:同时使用 cache 和 revalidate
const data = await fetch('/api/data', {
cache: 'no-store', // 这会忽略 revalidate
revalidate: 3600,
});
解决:根据需求选择正确的缓存策略。
| 场景 | cache | revalidate |
|---|---|---|
| 静态内容 | ’force-cache’ | 设置数值 |
| 实时数据 | ’no-store’ | 不设置 |
| ISR | ’force-cache’ | 设置数值 |
// ✅ 正确:ISR 模式
const data = await fetch('/api/data', {
next: { revalidate: 60 }, // 每60秒重新生成
});
// ✅ 正确:SSR 模式
const data = await fetch('/api/data', {
cache: 'no-store',
});
踩坑 3:Next.js Image 组件使用不当
问题:图片加载出现布局偏移(CLS),或图片不显示。
// ❌ 错误:未设置宽高
<Image src={post.coverImage} alt={post.title} />
解决:始终为已知尺寸的图片设置宽高,或使用 fill 配合父容器。
// ✅ 方案一:已知尺寸
<Image
src={post.coverImage}
alt={post.title}
width={800}
height={400}
/>
// ✅ 方案二:响应式(父容器需要 position: relative)
<div className="relative w-full h-64">
<Image
src={post.coverImage}
alt={post.title}
fill
className="object-cover"
/>
</div>
踩坑 4:环境变量使用不当
问题:客户端代码中使用了 .env 中的变量,导致泄露或 undefined。
解决:只有以 NEXT_PUBLIC_ 开头的变量才会暴露到客户端。
# .env.local
# ❌ 错误:私钥暴露(不会报错,但客户端无法访问)
DATABASE_URL=postgres://...
# ✅ 正确:客户端可见需要前缀
NEXT_PUBLIC_API_URL=https://api.example.com
// ✅ 服务器组件中,两种都能用
export default async function Page() {
const secret = process.env.DATABASE_URL; // ✓ 可用
const publicUrl = process.env.NEXT_PUBLIC_API_URL; // ✓ 可用
return <div>{publicUrl}</div>;
}
// ✅ 客户端组件中,只有 NEXT_PUBLIC_ 可用
'use client';
export function ClientComponent() {
const url = process.env.NEXT_PUBLIC_API_URL; // ✓ 可用
// const secret = process.env.DATABASE_URL; // ✗ undefined
return <div>{url}</div>;
}
踩坑 5:动态路由参数类型问题
问题:在 Next.js 15+ 中,params 变成了 Promise,需要 await。
// ❌ 错误:Next.js 15+ 直接使用 params
export default function Page({ params }) {
const { slug } = params;
// 可能是 undefined 或 Promise
}
// ✅ 正确:Next.js 15+ 需要 await
export default async function Page({ params }) {
const { slug } = await params;
const post = await getPost(slug);
return <article>{post.title}</article>;
}
性能优化实战技巧
1. 使用 next/script 优化第三方脚本
import Script from 'next/script';
export default function Page() {
return (
<>
<Script
src="https://analytics.example.com/script.js"
strategy="lazyOnload" // 空闲时加载
/>
</>
);
}
strategy 选项:
beforeInteractive:最优先(关键脚本)afterInteractive:默认(大多数场景)lazyOnload:空闲时加载(非关键)
2. 路由预加载
import Link from 'next/link';
import { prefetch } from 'next/navigation';
// 方法一:Link 组件自动预加载
<Link href="/about">About</Link>
// 方法二:手动预加载
<Link
href="/products/[id]"
as={`/products/${product.id}`}
prefetch={true} // 默认 true
>
{product.name}
</Link>
3. 流式渲染 (Streaming)
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<Header />
<Suspense fallback={<Loading />}>
<HeavyComponent />
</Suspense>
<Footer />
</div>
);
}
总结
Next.js 的渲染模式选择:
| 模式 | 构建时 | 请求时 | 适用场景 |
|---|---|---|---|
| SSG | ✓ | 静态内容 | |
| ISR | ✓ | ✓ | 频繁更新的静态内容 |
| SSR | ✓ | 个性化/实时数据 | |
| RSC | ✓ | ✓ | 混合场景 |
记住:没有最好的模式,只有最适合的模式。根据你的具体场景选择合适的渲染策略,结合本文的踩坑经验,相信你能在 Next.js 项目中少走很多弯路。
Happy coding! 🚀