Next.js服务端渲染实战:数据获取策略与流式渲染性能优化

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/

(0)
小编小编
上一篇 7小时前
下一篇 7小时前

相关推荐