React Server Components服务端渲染架构与Next.js App Router实战

React Server Components(RSC)是React 18引入的渲染范式,将组件拆分为服务端组件和客户端组件,服务端组件在服务器上执行不发送JavaScript到浏览器。Next.js App Router是对RSC的完整工程化实现。本文解析RSC工作原理,给出Next.js App Router的实践方案。

React Server Components运行机制与渲染流程

传统SSR在服务端渲染HTML,但所有组件的JavaScript仍然发送到客户端进行hydration。RSC不同——服务端组件的代码不包含在客户端bundle中,只有客户端组件的JavaScript会发送到浏览器。

// app/products/page.tsx - 服务端组件(默认)
// 文件顶部无需 "use client" 声明

import { db } from '@/lib/db'
import ProductCard from './product-card'
import { Suspense } from 'react'

// 服务端组件可直接访问数据库,无需API层
async function getProducts() {
    const products = await db.product.findMany({
        orderBy: { createdAt: 'desc' },
        take: 20
    })
    return products
}

export default async function ProductsPage() {
    const products = await getProducts()
    
    return (
        <div className="grid grid-cols-3 gap-6">
            {products.map(product => (
                <ProductCard key={product.id} product={product} />
            ))}
        </div>
    )
}

渲染过程中React Server为每个服务端组件生成一个序列化的RSC Payload,包含组件树结构和数据。客户端组件通过引用标记嵌入到RSC Payload中,浏览器收到Payload后渲染客户端组件并执行交互逻辑。服务端组件可以使用async/await直接获取数据,不需要useEffect和loading states。

客户端组件与服务端组件的边界划分

使用”use client”指令声明客户端组件。只有需要交互(onClick、useState、useEffect等)的组件才标记为客户端组件:

// app/products/product-card.tsx
"use client"

import { useState } from 'react'
import { useRouter } from 'next/navigation'

interface ProductCardProps {
    product: {
        id: string
        name: string
        price: number
        image: string
    }
}

export default function ProductCard({ product }: ProductCardProps) {
    const [isFavorited, setIsFavorited] = useState(false)
    const router = useRouter()
    
    const handleFavorite = async () => {
        setIsFavorited(!isFavorited)
        await fetch(`/api/favorites/${product.id}`, {
            method: isFavorited ? 'DELETE' : 'POST',
        })
    }
    
    return (
        <div className="border rounded-lg p-4">
            <img src={product.image} alt={product.name} />
            <h3>{product.name}</h3>
            <p>¥{product.price}</p>
            <button onClick={handleFavorite}>
                {isFavorited ? '已收藏' : '收藏'}
            </button>
        </div>
    )
}

组件边界的核心原则是将”use client”尽量下推到叶子节点。一个服务端组件可以渲染客户端组件,客户端组件也可以从服务端组件接收children prop。这种组合模式减少了客户端JavaScript体积:

// app/layout.tsx - 根布局(服务端组件)
import { Analytics } from './analytics'  // 客户端组件
import Header from './header'

export default function RootLayout({ children }) {
    return (
        <html>
            <body>
                <Header />
                {/* children来自服务端渲染的页面 */}
                {children}
                {/* Analytics的JS发送到客户端,但不影响页面内容渲染 */}
                <Analytics />
            </body>
        </html>
    )
}

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

App Router中fetch默认启用缓存,通过cache选项控制缓存行为:

// 完全静态化 - 构建时获取,缓存到CDN
const data = await fetch('https://api.example.com/data', {
    cache: 'force-cache'  // 默认行为
})

// 每次请求重新获取 - 不缓存
const data = await fetch('https://api.example.com/data', {
    cache: 'no-store'
})

// ISR增量静态再生 - 60秒后后台重新生成
const data = await fetch('https://api.example.com/data', {
    next: { revalidate: 60 }
})

// 路由段级配置
// app/blog/[slug]/page.tsx
export const revalidate = 3600    // ISR每小时重新生成
export const dynamic = 'force-static'  // 强制静态
export const fetchCache = 'default-cache'

export default async function BlogPost({ params }) {
    const post = await fetch(
        `https://api.example.com/posts/${params.slug}`,
        { next: { tags: ['blog-post', `post-${params.slug}`] } }
    ).then(r => r.json())
    
    return <article>{post.content}</article>
}

流式渲染与Suspense边界配置

App Router通过Suspense实现流式渲染,将页面分割为多个独立的流式块,先渲染的部分立即发送到浏览器:

// app/dashboard/page.tsx
import { Suspense } from 'react'
import RevenueChart from './revenue-chart'
import UserTable from './user-table'
import StatsCards from './stats-cards'

export default function Dashboard() {
    return (
        <div>
            {/* StatsCards先加载,立即渲染 */}
            <Suspense fallback={<SkeletonGrid />}>
                <StatsCards />
            </Suspense>
            
            {/* RevenueChart较慢,不阻塞UserTable */}
            <div className="grid grid-cols-2 gap-6 mt-6">
                <Suspense fallback={<ChartSkeleton />}>
                    <RevenueChart />
                </Suspense>
                
                <Suspense fallback={<TableSkeleton />}>
                    <UserTable />
                </Suspense>
            </div>
        </div>
    )
}

// 慢查询组件 - 模拟延迟
async function RevenueChart() {
    const data = await fetch('https://api.example.com/revenue', {
        cache: 'no-store'
    }).then(r => r.json())
    
    return <Chart data={data} />
}

每个Suspense边界独立流式渲染,浏览器收到第一个chunk就开始渲染对应区域。相比传统SSR等待整个页面数据加载完成再发送HTML,流式渲染显著改善首屏内容可绘制时间(FCP)和最大内容绘制时间(LCP)。

实测一个包含3个数据源的Dashboard页面,传统SSR模式下FCP约2.8秒,流式渲染模式下FCP降到0.6秒(StatsCards先渲染),LCP从3.2秒降到1.3秒。关键是Suspense边界要按照数据获取速度分层级,快查询放在外层先渲染,慢查询放在内层延迟渲染。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/reactservercomponents-fu-wu-duan-xuan-ran-jia-gou-yu/

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

相关推荐