React Server Components服务端组件原理与Streaming SSR流式渲染实战

React Server Components(RSC)是React 18引入的组件渲染架构,将组件分为Server Components和Client Components两类。Server Components在服务端渲染,零JavaScript发送到客户端,适合数据获取和静态内容展示。Streaming SSR将页面分块流式发送到浏览器,配合Suspense实现渐进式渲染,首屏内容无需等待全部数据加载完成。这套架构在Next.js App Router中得到完整实现,显著提升Web性能优化的LCP和FCP指标。

React Server Components与Client Components的边界划分

Server Components运行在服务端,可以直连数据库、读取文件系统、使用服务端密钥,但不能使用useState、useEffect等客户端Hooks,也不能监听浏览器事件。Client Components通过文件顶部”use client”指令声明,运行在浏览器端,拥有完整React能力。

// app/products/page.tsx - Server Component(默认)
// 直接访问数据库,无需API层
import { db } from "@/lib/db"

export default async function ProductsPage() {
  // 服务端直接查询数据库
  const products = await db.product.findMany({
    take: 20,
    orderBy: { createdAt: "desc" }
  })

  return (
    <div>
      <h1>产品列表</h1>
      <ul>
        {products.map(product => (
          <li key={product.id}>
            <h2>{product.name}</h2>
            <p>{product.description}</p>
            <span>¥{product.price}</span>
          </li>
        ))}
      </ul>
    </div>
  )
}
// app/components/AddToCartButton.tsx - Client Component
"use client"

import { useState } from "react"

export default function AddToCartButton({ productId }: { productId: number }) {
  const [adding, setAdding] = useState(false)

  const handleAdd = async () => {
    setAdding(true)
    await fetch("/api/cart", {
      method: "POST",
      body: JSON.stringify({ productId })
    })
    setAdding(false)
  }

  return (
    <button onClick={handleAdd} disabled={adding}>
      {adding ? "添加中..." : "加入购物车"}
    </button>
  )
}

Streaming SSR流式渲染与Suspense配合机制

传统SSR需要等待页面所有数据获取完成后,一次性将完整HTML发送到浏览器。Streaming SSR将页面拆分为多个chunk,每个chunk准备好就立即发送,浏览器可以在收到首个chunk时就开始渲染。

// app/dashboard/layout.tsx
import { Suspense } from "react"

export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <nav>导航栏(立即渲染)</nav>
        <main>
          {/* 用户信息:优先加载,立即显示 */}
          <Suspense fallback={<div>加载用户信息...</div>}>
            <UserProfile />
          </Suspense>

          {/* 订单列表:数据量大,慢加载,不阻塞首屏 */}
          <Suspense fallback={<OrderSkeleton />}>
            <OrderList />
          </Suspense>

          {/* 数据图表:最慢,用骨架屏占位 */}
          <Suspense fallback={<ChartSkeleton />}>
            <SalesChart />
          </Suspense>
        </main>
      </body>
    </html>
  )
}

// 服务端异步组件,数据获取完成前Suspense显示fallback
async function OrderList() {
  const orders = await fetch("https://api.example.com/orders", {
    next: { revalidate: 60 }  // ISR:60秒缓存
  }).then(r => r.json())

  return (
    <table>
      {orders.map(order => (
        <tr key={order.id}>
          <td>{order.id}</td>
          <td>{order.status}</td>
          <td>{order.total}</td>
        </tr>
      ))}
    </table>
  )
}

Next.js App Router数据获取与缓存策略

fetch扩展与缓存控制

Next.js扩展了原生fetch,增加缓存控制选项:

// 强制缓存,永不过期(适合不常变的内容)
const data = await fetch(url, { cache: "force-cache" })

// 每次请求都重新获取(适合实时数据)
const data = await fetch(url, { cache: "no-store" })

// ISR:按时间间隔重新验证
const data = await fetch(url, { next: { revalidate: 3600 } }) // 1小时

// 按需重新验证
import { revalidateTag } from "next/cache"

// 获取时打标签
const data = await fetch(url, { next: { tags: ["products"] } })

// 数据更新后手动触发重新验证
revalidateTag("products")

并行与串行数据获取

// 并行获取(推荐):Promise.all同时请求
async function Dashboard() {
  const [user, stats, notifications] = await Promise.all([
    fetch("/api/user").then(r => r.json()),
    fetch("/api/stats").then(r => r.json()),
    fetch("/api/notifications").then(r => r.json())
  ])

  return (
    <>
      <UserInfo user={user} />
      <Stats data={stats} />
      <Notifications items={notifications} />
    </>
  )
}

// 串行获取(有依赖关系时):配合Suspense避免瀑布效应
async function ProductDetail({ id }) {
  return (
    <>
      <Suspense fallback={<Skeleton />}>
        <ProductInfo id={id} />
      </Suspense>
      <Suspense fallback={<ReviewSkeleton />}>
        <ProductReviews id={id} />  {/* 依赖ProductInfo中的productId */}
      </Suspense>
    </>
  )
}

Server Actions:服务端操作的无API路由模式

Server Actions允许在Client Component中直接调用服务端函数,无需编写API路由:

// app/actions.ts
"use server"

import { db } from "@/lib/db"
import { revalidatePath } from "next/cache"

export async function createProduct(formData: FormData) {
  const name = formData.get("name") as string
  const price = parseFloat(formData.get("price") as string)

  await db.product.create({
    data: { name, price }
  })

  // 刷新页面缓存
  revalidatePath("/products")
}

// app/products/create/page.tsx
import { createProduct } from "@/app/actions"

export default function CreateProductPage() {
  return (
    <form action={createProduct}>
      <input name="name" type="text" placeholder="产品名称" />
      <input name="price" type="number" placeholder="价格" />
      <button type="submit">创建</button>
    </form>
  )
}

性能优化:RSC包体积缩减与加载策略

Server Components的JavaScript不会发送到客户端,页面中仅Client Component的代码被打包。通过分析打包体积可以验证效果:

# 分析客户端打包体积
npx @next/bundle-analyzer

# 关键指标:
# - First Load JS: 首屏加载的JS体积
# - 目标: 首屏JS < 200KB(gzip后)

动态导入Client Components可以进一步减小首屏包体积:

import dynamic from "next/dynamic"

// 按需加载,不阻塞首屏
const HeavyChart = dynamic(() => import("./HeavyChart"), {
  loading: () => <div>加载图表...</div>,
  ssr: false  // 纯客户端渲染,不参与SSR
})

RSC架构下,数据获取在服务端完成,客户端无需额外请求。页面跳转时,Next.js通过RSC payload增量更新组件,只传输变化部分,实现接近原生App的页面切换体验。配合prefetch预取策略,常用路由的RSC payload在用户点击前就预加载完成。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/reactservercomponents-fu-wu-duan-zu-jian-yuan-li-yu/

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

相关推荐