Next.js在App Router架构下重新设计了服务端渲染机制,引入React Server Components和流式渲染(Streaming SSR),将数据获取、渲染、传输三个阶段解耦。合理利用数据获取策略和Suspense边界,可以显著改善首屏加载性能和用户体验。
App Router数据获取模式对比
Next.js App Router提供三种数据获取方式,对应不同的渲染时机和缓存策略。
Server Components中的async/await是最直接的方式,在服务端渲染时阻塞等待数据:
// app/products/page.tsx
async function getProducts() {
const res = await fetch('https://api.example.com/products', {
next: {
revalidate: 3600, // ISR:每小时重新验证
tags: ['products'] // 按标签缓存,支持按需刷新
}
})
return res.json()
}
export default async function ProductsPage() {
const products = await getProducts()
return (
<div>
{products.map(p => (
<ProductCard key={p.id} product={p} />
))}
</div>
)
}
fetch函数的缓存控制选项:
- cache: ‘force-cache’ —— 默认行为,缓存响应直到revalidate过期
- cache: ‘no-store’ —— 不缓存,每次请求重新获取(动态渲染)
- next.revalidate: N —— ISR模式,N秒后标记为stale
- next.tags: [‘tag’] —— 按标签分组,通过revalidateTag()精准刷新
并行数据获取避免瀑布请求:
// 串行:慢,总耗时 = 请求A + 请求B
export default async function Page() {
const user = await getUser() // 200ms
const orders = await getOrders(user.id) // 300ms
// 总计 500ms
}
// 并行:快,总耗时 = max(请求A, 请求B)
export default async function Page() {
const [user, products] = await Promise.all([
getUser(), // 200ms
getProducts() // 300ms
])
// 总计 300ms
}
// 部分串行优化:先获取不依赖的数据
export default async function Page() {
const products = getProducts() // 不await,返回Promise
const user = await getUser() // 等待用户数据
const productList = await products // 此时products可能已完成
return <UserOrders user={user} products={productList} />
}
流式渲染与Suspense边界设计
流式渲染将页面拆分为多个chunk,服务端先发送HTML骨架和已完成的组件,未完成的部分通过Suspense占位,数据就绪后通过流式注入。用户无需等待所有数据加载完成即可看到部分内容。
// app/dashboard/page.tsx
import { Suspense } from 'react'
export default function DashboardPage() {
return (
<div>
<Header /> {/* 静态内容,立即渲染 */}
<Suspense fallback={<MetricsSkeleton />}>
<Metrics /> {/* 慢查询,流式渲染 */}
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart /> {/* 另一个慢查询 */}
</Suspense>
<Suspense fallback={<ListSkeleton />}>
<RecentOrders /> {/* 第三个慢查询 */}
</Suspense>
</div>
)
}
async function Metrics() {
const data = await getMetrics() // 耗时 800ms
return <MetricsView data={data} />
}
async function RevenueChart() {
const data = await getRevenue() // 耗时 1200ms
return <Chart data={data} />
}
async function RecentOrders() {
const data = await getOrders() // 耗时 600ms
return <OrderList orders={data} />
}
每个Suspense边界独立流式传输。浏览器收到的HTML流大致如下:
<!-- 第一chunk:立即发送 -->
<div>Dashboard</div>
<div class="header">...</div>
<div class="skeleton">Loading metrics...</div>
<div class="skeleton">Loading chart...</div>
<div class="skeleton">Loading orders...</div>
<!-- 第二chunk:600ms后,RecentOrders就绪 -->
<template id="B:orders">
<div>Order #1 ...</div>
</template>
<script>$RC("B:orders")</script>
<!-- 第三chunk:800ms后,Metrics就绪 -->
<template id="B:metrics">
<div>Total: $12,345</div>
</template>
<script>$RC("B:metrics")</script>
<!-- 第四chunk:1200ms后,RevenueChart就绪 -->
<template id="B:chart">
<svg>...</svg>
</template>
<script>$RC("B:chart")</script>
React通过Streaming SSR协议实现部分HTML的渐进式替换,无需额外客户端代码。骨架屏组件应保持与实际内容一致的尺寸,避免布局偏移(CLS)。
loading.tsx与平行路由优化
Next.js约定文件loading.tsx自动包裹对应路由段的内容在Suspense中,无需手动添加Suspense边界:
// app/dashboard/loading.tsx
export default function Loading() {
return (
<div className="space-y-4">
<div className="h-8 bg-gray-200 rounded animate-pulse" />
<div className="grid grid-cols-3 gap-4">
<div className="h-32 bg-gray-200 rounded animate-pulse" />
<div className="h-32 bg-gray-200 rounded animate-pulse" />
<div className="h-32 bg-gray-200 rounded animate-pulse" />
</div>
</div>
)
}
平行路由(Parallel Routes)允许在同一页面中独立流式加载多个Slot,每个Slot有自己的loading状态和错误边界:
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
team,
}: {
children: React.ReactNode
analytics: React.ReactNode
team: React.ReactNode
}) {
return (
<div className="flex">
<main className="flex-1">{children}</main>
<aside className="w-80">
{analytics}
{team}
</aside>
</div>
)
}
// app/dashboard/@analytics/page.tsx
export default async function Analytics() {
const data = await getAnalytics() // 独立加载
return <AnalyticsPanel data={data} />
}
// app/dashboard/@analytics/loading.tsx
export default function Loading() {
return <AnalyticsSkeleton />
}
按需缓存刷新与标签管理
ISR模式下通过revalidateTag和revalidatePath在数据变更时主动刷新缓存:
// app/actions/revalidate.ts
'use server'
import { revalidateTag, revalidatePath } from 'next/cache'
export async function refreshProducts() {
revalidateTag('products')
// 下次请求时重新获取数据
}
export async function refreshProductPage(productId: string) {
revalidatePath(`/products/${productId}`)
revalidatePath('/products', 'page')
}
// 在API路由中调用
export async function POST(request: Request) {
const body = await request.json()
await db.product.update({
where: { id: body.id },
data: body
})
revalidateTag('products')
revalidateTag(`product-${body.id}`)
return Response.json({ success: true })
}
客户端组件与服务端组件数据传递
Server Components获取的数据传递给Client Components时,不可传递函数、Date对象等不可序列化的值。需要序列化为纯数据:
// Server Component
export default async function Page() {
const products = await getProducts()
const serializedProducts = products.map(p => ({
id: p.id,
name: p.name,
price: p.price,
createdAt: p.createdAt.toISOString(), // Date转字符串
}))
return <ProductEditor products={serializedProducts} />
}
// Client Component
'use client'
export function ProductEditor({ products }: {
products: Array<{
id: string
name: string
price: number
createdAt: string
}>
}) {
const [items, setItems] = useState(products)
return (
<div>
{items.map(p => (
<div key={p.id}>{p.name} - {p.price}</div>
))}
</div>
)
}
对于需要实时交互的数据,避免在Server Component中获取,改为在Client Component中使用SWR或React Query获取。Server Component只负责初始HTML渲染,交互态由客户端管理,两者通过props桥接,实现首屏性能与交互体验的平衡。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/nextjs-fu-wu-duan-xuan-ran-shi-zhan-shu-ju-huo-qu-ce-lyue/