Skip to content

静态与动态渲染

🙋 页面应该在构建时生成还是请求时生成?如何平衡性能和数据新鲜度?

渲染模式概览#

模式全称生成时机适用场景
SSGStatic Site Generation构建时内容不常变化
SSRServer-Side Rendering请求时实时数据
ISRIncremental Static Regeneration构建 + 增量更新折中方案

静态渲染(SSG)#

默认行为#

没有动态函数的页面默认静态渲染:

app/about/page.tsx
// Next.js 15.x
// 这是静态页面,构建时生成
export default function AboutPage() {
return (
<main>
<h1>关于我们</h1>
<p>这是静态内容,构建时生成。</p>
</main>
)
}

带数据的静态页面#

app/posts/page.tsx
// Next.js 15.x
async function getPosts() {
// 默认行为:构建时获取,永久缓存
const res = await fetch('https://api.example.com/posts')
return res.json()
}
export default async function PostsPage() {
const posts = await getPosts()
return (
<ul>
{posts.map((post: any) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}

动态路由静态生成#

使用 generateStaticParams 预生成路径:

app/blog/[slug]/page.tsx
// Next.js 15.x
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then((r) =>
r.json()
)
return posts.map((post: { slug: string }) => ({
slug: post.slug,
}))
}
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await fetch(`https://api.example.com/posts/${slug}`).then((r) =>
r.json()
)
return <article>{post.content}</article>
}

🎯 效果:构建时为每个 slug 生成静态 HTML。

动态渲染(SSR)#

使用动态函数#

以下函数会触发动态渲染:

app/dashboard/page.tsx
// Next.js 15.x
import { cookies, headers } from 'next/headers'
export default async function DashboardPage() {
// 使用 cookies() 触发动态渲染
const cookieStore = await cookies()
const token = cookieStore.get('token')
// 使用 headers() 也会触发
const headersList = await headers()
const userAgent = headersList.get('user-agent')
return <div>动态内容</div>
}

禁用缓存#

app/realtime/page.tsx
// Next.js 15.x
async function getData() {
const res = await fetch('https://api.example.com/data', {
cache: 'no-store', // 禁用缓存,每次请求都获取新数据
})
return res.json()
}
export default async function RealtimePage() {
const data = await getData()
return <div>{data.value}</div>
}

路由段配置#

app/live/page.tsx
// Next.js 15.x
// 强制动态渲染
export const dynamic = 'force-dynamic'
// 或强制静态渲染
// export const dynamic = 'force-static'
export default function LivePage() {
return <div>实时页面</div>
}

增量静态再生成(ISR)#

基于时间的重新验证#

app/news/page.tsx
// Next.js 15.x
async function getNews() {
const res = await fetch('https://api.example.com/news', {
next: { revalidate: 60 }, // 60 秒后重新验证
})
return res.json()
}
export default async function NewsPage() {
const news = await getNews()
return (
<ul>
{news.map((item: any) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
)
}

🎯 行为

  1. 首次请求:返回缓存的 HTML
  2. 60 秒内的请求:继续返回缓存
  3. 60 秒后的请求:返回缓存,同时后台重新生成
  4. 下次请求:返回新生成的 HTML

路由级别配置#

app/products/page.tsx
// Next.js 15.x
// 整个路由段的重新验证时间
export const revalidate = 3600 // 1 小时
export default async function ProductsPage() {
const products = await fetch('https://api.example.com/products').then((r) =>
r.json()
)
return <div>{/* 产品列表 */}</div>
}

按需重新验证#

app/api/revalidate/route.ts
// Next.js 15.x
import { revalidatePath, revalidateTag } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'
export async function POST(request: NextRequest) {
const { path, tag, secret } = await request.json()
// 验证密钥
if (secret !== process.env.REVALIDATE_SECRET) {
return NextResponse.json({ error: 'Invalid secret' }, { status: 401 })
}
// 按路径重新验证
if (path) {
revalidatePath(path)
}
// 按标签重新验证
if (tag) {
revalidateTag(tag)
}
return NextResponse.json({ revalidated: true })
}
app/blog/[slug]/page.tsx
// Next.js 15.x
async function getPost(slug: string) {
const res = await fetch(`https://api.example.com/posts/${slug}`, {
next: { tags: ['posts', `post-${slug}`] }, // 添加缓存标签
})
return res.json()
}

调用 API 触发重新验证:

Terminal window
curl -X POST http://localhost:3000/api/revalidate \
-H "Content-Type: application/json" \
-d '{"tag": "posts", "secret": "your-secret"}'

缓存配置详解#

fetch 缓存选项#

// 默认:永久缓存(SSG)
fetch('https://api.example.com/data')
// 禁用缓存(SSR)
fetch('https://api.example.com/data', { cache: 'no-store' })
// 定时重新验证(ISR)
fetch('https://api.example.com/data', { next: { revalidate: 60 } })
// 缓存标签
fetch('https://api.example.com/data', { next: { tags: ['tag1'] } })

组合使用#

app/dashboard/page.tsx
// Next.js 15.x
async function getUser() {
// 用户信息:每次请求都获取
const res = await fetch('https://api.example.com/me', {
cache: 'no-store',
})
return res.json()
}
async function getStats() {
// 统计数据:5 分钟缓存
const res = await fetch('https://api.example.com/stats', {
next: { revalidate: 300 },
})
return res.json()
}
async function getConfig() {
// 配置:构建时获取,永久缓存
const res = await fetch('https://api.example.com/config')
return res.json()
}
export default async function DashboardPage() {
const [user, stats, config] = await Promise.all([
getUser(),
getStats(),
getConfig(),
])
return <div>{/* 使用数据渲染 */}</div>
}

选择渲染模式#

决策流程#

数据需要实时更新?
├─ 是 → 需要个性化内容?
│ ├─ 是 → SSR(动态渲染)
│ └─ 否 → ISR(定时更新)
└─ 否 → SSG(静态渲染)

场景示例#

页面类型推荐模式原因
关于页面SSG内容固定
博客文章ISR偶尔更新
商品列表ISR定期同步
用户仪表盘SSR个性化数据
购物车SSR实时状态
营销落地页SSG内容固定

实战配置#

电商产品页#

app/products/[id]/page.tsx
// Next.js 15.x
// 预生成热门产品
export async function generateStaticParams() {
const topProducts = await fetch(
'https://api.example.com/products/top100'
).then((r) => r.json())
return topProducts.map((p: { id: string }) => ({ id: p.id }))
}
// 允许未预生成的路径动态生成
export const dynamicParams = true
// 产品信息 1 小时更新
async function getProduct(id: string) {
const res = await fetch(`https://api.example.com/products/${id}`, {
next: { revalidate: 3600, tags: [`product-${id}`] },
})
return res.json()
}
// 库存实时获取
async function getStock(id: string) {
const res = await fetch(`https://api.example.com/products/${id}/stock`, {
cache: 'no-store',
})
return res.json()
}
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const [product, stock] = await Promise.all([getProduct(id), getStock(id)])
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p>库存: {stock.quantity}</p>
</div>
)
}

新闻网站首页#

app/page.tsx
// Next.js 15.x
// 头条新闻:5 分钟更新
async function getHeadlines() {
const res = await fetch('https://api.example.com/headlines', {
next: { revalidate: 300, tags: ['headlines'] },
})
return res.json()
}
// 热门文章:1 小时更新
async function getTrending() {
const res = await fetch('https://api.example.com/trending', {
next: { revalidate: 3600, tags: ['trending'] },
})
return res.json()
}
// 天气:15 分钟更新
async function getWeather() {
const res = await fetch('https://api.example.com/weather', {
next: { revalidate: 900 },
})
return res.json()
}
export default async function HomePage() {
const [headlines, trending, weather] = await Promise.all([
getHeadlines(),
getTrending(),
getWeather(),
])
return (
<main>
<section>
<h2>头条新闻</h2>
{/* 渲染头条 */}
</section>
<section>
<h2>热门文章</h2>
{/* 渲染热门 */}
</section>
<aside>
<h3>天气</h3>
{/* 渲染天气 */}
</aside>
</main>
)
}

常见问题#

🤔 Q: 如何查看页面是静态还是动态?

构建时查看输出:

🤔 Q: ISR 的 revalidate 是精确时间吗?

不是。revalidate 是最小时间间隔,实际更新发生在间隔后的第一次请求。

🤔 Q: 如何强制所有 fetch 使用相同缓存策略?

app/layout.tsx
export const fetchCache = 'force-cache' // 或 'force-no-store'

恭喜你完成了渲染模式的学习!下一篇将进入数据处理部分,介绍数据获取的最佳实践。

-EOF-