React Server Components从原理到落地:RSC渲染机制与迁移实践

React Server Components的渲染模型

React Server Components(RSC)是React 18引入、Next.js App Router中落地的渲染架构。核心变化是将组件分为Server Components和Client Components两种类型,Server Components在服务器端渲染,输出结果是一个序列化的RSC Payload而非HTML,由Client Components在客户端消费。

与SSR的区别——SSR在服务端将整个组件树渲染为HTML字符串,客户端hydration后所有组件都变成客户端组件。RSC则区分服务端和客户端,Server Components的数据获取在服务端完成,不会打包到客户端bundle中,JavaScript包体积因此显著减小。

Server Components与Client Components的边界

组件是Server还是Client由文件位置和声明决定。在Next.js App Router中,app/目录下的组件默认是Server Component。使用'use client'指令声明Client Component:

// app/page.tsx — Server Component(默认)
import { db } from '@/lib/db'
import ProductCard from './ProductCard'

export default async function Page() {
  // 直接在组件中访问数据库,不需要getServerSideProps
  const products = await db.product.findMany({
    take: 20,
    orderBy: { createdAt: 'desc' }
  })
  
  return (
    <div>
      {products.map(p => (
        <ProductCard key={p.id} product={p} />
      ))}
    </div>
  )
}
// app/ProductCard.tsx — Client Component
'use client'

import { useState } from 'react'

export default function ProductCard({ product }) {
  const [liked, setLiked] = useState(false)
  
  return (
    <div onClick={() => setLiked(!liked)}>
      <h3>{product.name}</h3>
      <p>{product.price}</p>
    </div>
  )
}

划分原则——需要在客户端交互(useState、useEffect、事件监听)的组件标为Client Component,纯粹展示数据和组合子组件的保持为Server Component。Client Component不意味着只在客户端渲染——它仍会在服务端预渲染为HTML。区别在于Client Component的代码会打包到客户端bundle中。

数据获取方式的范式转变

RSC架构下不再需要getServerSidePropsgetStaticProps等数据获取函数。Server Component可以直接使用async/await获取数据,因为渲染过程在服务端执行:

// app/dashboard/page.tsx
import { cache } from 'react'

// 使用cache函数实现请求级别的数据去重
const getUser = cache(async (userId: string) => {
  const res = await fetch(`${process.env.API_URL}/users/${userId}`, {
    next: { tags: ['user'] }  // 用于按需重新验证
  })
  return res.json()
})

export default async function Dashboard({
  params
}: {
  params: { userId: string }
}) {
  const user = await getUser(params.userId)
  const orders = await fetch(
    `${process.env.API_URL}/orders?userId=${params.userId}`,
    { cache: 'no-store' }  // 每次请求都重新获取
  ).then(r => r.json())
  
  return (
    <main>
      <h1>{user.name}的控制台</h1>
      <OrderList orders={orders} />
    </main>
  )
}

React的cache函数实现的是请求级缓存——同一个请求中多次调用getUser只执行一次实际获取。这与Next.js的fetch缓存不同,后者是跨请求的数据缓存。

从Pages Router迁移到App Router

迁移不应该一次性完成。推荐的渐进迁移策略:

  1. 保留pages/目录——Next.js支持App Router和Pages Router共存,新页面用App Router,旧页面维持不变。
  2. 新增路由用App Router——所有新功能在app/目录下开发,逐步替换旧路由。
  3. 共享组件抽离——将components/目录下的组件标记为'use client',确保与App Router兼容。
  4. 逐步迁移旧页面——按页面优先级逐个迁移,每迁移一个页面就运行完整测试。

迁移中最常见的问题是useRouter的差异。Pages Router的useRouter来自next/router,App Router需要从next/navigation导入:

// Pages Router(旧)
import { useRouter } from 'next/router'

// App Router(新)
'use client'
import { useRouter } from 'next/navigation'
import { useParams } from 'next/navigation'

export default function Navigation() {
  const router = useRouter()
  const params = useParams()
  
  return (
    <button onClick={() => router.push('/dashboard')}>
      进入控制台
    </button>
  )
}

RSC Payload与流式渲染

Server Components渲染的输出不是HTML,而是RSC Payload——一种特殊的序列化格式。浏览器收到RSC Payload后,React在客户端将其重构为组件树并与Client Components合并。

流式渲染允许服务器逐步发送已渲染完成的部分,客户端可以提前开始渲染。通过React的Suspense组件实现:

// app/page.tsx
import { Suspense } from 'react'
import ProductList from './ProductList'
import RecommendedList from './RecommendedList'

export default function Page() {
  return (
    <main>
      <h1>商品首页</h1>
      <Suspense fallback={<div>加载商品列表...</div>}>
        <ProductList />  {/* 延迟加载 */}
      </Suspense>
      <Suspense fallback={<div>加载推荐...</div>}>
        <RecommendedList />  {/* 延迟加载 */}
      </Suspense>
    </main>
  )
}

关键点:每个Suspense边界独立流式渲染,一个组件的数据未就绪不会阻塞其他组件。用户看到的是先加载的部分先显示,而非等待所有数据加载完毕才开始渲染。这直接改善了首屏时间(FCP)和可交互时间(TTI)。

客户端bundle体积优化效果

RSC对bundle体积的优化是实质性的。一个典型电商首页迁移到RSC后的变化:

  • 日期格式化库date-fns(35KB)从客户端bundle移除——在Server Component中格式化日期后传递字符串
  • Markdown渲染库remark(120KB)从客户端bundle移除——在Server Component中渲染为HTML后传递
  • 图标库lucide-react按需引入,仅客户端需要的图标打包(tree-shaking效果在Server Component中更彻底)

验证方式是通过Next.js内置的bundle分析:

# package.json
"scripts": {
  "analyze": "ANALYZE=true next build"
}

# next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true'
})
module.exports = withBundleAnalyzer({})

执行npm run analyze后生成可视化报告,对比迁移前后的First Load JS体积。通常数据展示密集型页面的First Load JS减少30%-50%。

常见问题与排查

Server Component中使用了useState报错——Server Component不支持任何hooks。如果组件需要状态管理,必须声明为'use client'。另一个方案是将需要状态管理的部分拆分到Client Component子组件中。

Client Component中无法直接获取数据——Client Component不能是async函数。数据获取应该在父级Server Component中完成,通过props传递给Client Component。

第三方组件库不支持RSC——大量第三方组件库内部使用了hooks和浏览器API,无法在Server Component中使用。解决方式是用'use client'包裹第三方组件的Wrapper组件,在Server Component中引用Wrapper。

// app/components/DatePickerWrapper.tsx
'use client'
import DatePicker from 'react-datepicker'
import 'react-datepicker/dist/react-datepicker.css'

export default function DatePickerWrapper(props) {
  return <DatePicker {...props} />
}

前端工程化实践中,RSC的落地不是一蹴而就的迁移。理解Server与Client组件的渲染边界、数据流方向和打包规则,才能在Web性能优化中获得实际收益。TypeScript实战中建议为Server/Client组件建立类型约束,避免在Server Component中误用客户端API。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/reactservercomponents-cong-yuan-li-dao-luo-di-rsc-xuan-ran/

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

相关推荐