用代码示例和数据流讲清楚 Next.js App Router 下四种渲染策略的区别和实际用法。

用 Next.js 开发,核心问题之一就是:页面在哪里渲染、什么时候渲染。不同的策略在性能、SEO、数据实时性和服务器成本之间做取舍——为每个页面选对渲染方式,效果差距很大。
在开始之前,先说一个关键前提:Next.js App Router 默认使用 React Server Components(RSC)。所有组件默认在服务端渲染,不会向客户端发送任何 JavaScript,除非你显式加上 'use client'。这和旧的 Pages Router 有本质区别——也直接影响了下面四种策略的实际表现。
在 App Router 里,纯客户端渲染需要你主动选择。给组件加上 'use client',它会在服务端预渲染 HTML 后,在客户端进行注水(hydration)。
'use client'
import { useState, useEffect } from 'react'
export default function Dashboard() {
const [stats, setStats] = useState(null)
useEffect(() => {
fetch('/api/stats').then(res => res.json()).then(setStats)
}, [])
if (!stats) return <div>加载中...</div>
return <div>共 {stats.totalUsers} 位用户</div>
}数据流:
客户端 → 服务器(返回预渲染 HTML + JS 包)→ 客户端(注水)→ API → 客户端(用数据重新渲染)
注意,即使是 'use client' 组件,首次加载时也会有服务端渲染的 HTML——浏览器不会看到白屏。但数据获取仍然发生在客户端注水之后。
适用场景: 交互式组件、数据看板、需要登录的页面——需要浏览器 API 或实时状态的地方。
在 App Router 里,SSR 发生在 Server Component 动态获取数据时。默认情况下 fetch() 请求会被缓存——要让页面每次请求都重新渲染,需要用 cache: 'no-store' 或者标记页面为动态。
// app/news/page.tsx — 每次请求都重新渲染
export const dynamic = 'force-dynamic'
export default async function NewsPage() {
const res = await fetch('https://api.example.com/news', {
cache: 'no-store',
})
const articles = await res.json()
return (
<ul>
{articles.map(
数据流:
客户端 → 服务器 → 数据库/API → 服务器(构建 HTML)→ 客户端(直接展示)
用户马上就能看到完整页面,搜索引擎也能抓取到渲染好的内容。代价是每个请求都要经过服务器。
适用场景: 内容每次请求都可能不同的页面——新闻流、用户主页、搜索结果。
SSG 在构建时预渲染页面。在 App Router 里,这是默认行为——如果页面没有获取动态数据,它自动就是静态的。对于动态路由,用 generateStaticParams() 告诉 Next.js 需要预生成哪些路径。
// app/blog/[slug]/page.tsx — 构建时预渲染
export async function generateStaticParams() {
const posts = await getAllPostSlugs()
return posts.map(slug => ({ slug }))
}
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await
数据流:
构建时:服务器 → API/数据库 → 生成静态 HTML 请求时:客户端 → CDN(瞬间返回预生成页面)
这是速度最快的方案。内容在 build 时就固定了——数据更新后,用户看到的还是旧内容,直到下次部署。这个站的博客页面就是用 SSG 生成的——内容存在 MDX 文件里,只有我推送新 commit 才会更新。
适用场景: 博客文章、文档、营销页面——更新不频繁的内容。
ISR 兼具 SSG 的速度和无需全量重新部署就能更新内容的能力。导出一个 revalidate 间隔,Next.js 在过期前一直返回缓存页面。过期后,下一个请求触发后台重新生成。
// app/products/[id]/page.tsx — 静态页面,每 60 秒刷新
export const revalidate = 60
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await fetch(`https://api.example.com/products/${params.id}`, {
next: { revalidate: 60 },
}).then(res
数据流:
首次请求:客户端 → CDN(瞬间返回缓存 HTML) 后台:服务器 → API/数据库 → 生成并缓存新 HTML 下次请求:客户端 → CDN(返回更新后的页面)
小小的代价是:总有一个用户拿到的是触发重建的旧版本。但对绝大多数场景来说,几秒的延迟换来近乎瞬时的加载速度,完全值得。
适用场景: 商品详情页、列表页、博客首页——定期更新但不需要实时的内容。
从 Next.js 15 开始,还有第五种方案模糊了静态和动态的界限。部分预渲染(Partial Prerendering) 让一个页面可以同时包含静态和动态部分——静态外壳从 CDN 瞬间返回,动态内容在准备好后流式传入。
// app/page.tsx — 静态外壳 + 动态内容
import { Suspense } from 'react'
export default function HomePage() {
return (
<div>
<h1>欢迎</h1> {/* 静态 — 从 CDN 返回 */}
<Suspense fallback={<p>加载中...</p>}>
<RecommendedItems /> {/* 动态 — 流式传入 */}
</Suspense>
</
PPR 是这四种策略的自然演进——不再需要为整个页面选一种模式,而是在组件级别做精细控制。它还比较新,但代表了 Next.js 渲染的发展方向。
| 方式 | 首屏速度 | SEO | 数据实时性 | Next.js 配置 |
|---|---|---|---|---|
| CSR | 中等 | 尚可* | 实时(客户端) | 'use client' + useEffect |
| SSR | 中等 | 好 | 实时(每次请求) | dynamic = 'force-dynamic' |
| SSG | 快 | 好 | 构建时固定 | 默认 / generateStaticParams() |
| ISR | 快 | 好 | 定期刷新 | export const revalidate = N |
| PPR | 快 | 好 | 混合 | Suspense 边界 + 动态数据 |
*App Router 中 CSR 组件首次加载仍有服务端渲染的 HTML,所以 SEO 比传统 SPA 好。
能静态就静态,该动态才动态。把渲染的复杂度交给框架,你只管专注于真正重要的事——给用户创造价值。