
React Server Components 边界不是所有组件都该搬到服务端React Server Components 带来了新的架构选择。服务端组件可以减少客户端包体、直接访问后端资源也能让一些页面更快。但它不是把所有组件搬到服务端的按钮。交互状态、浏览器 API、动画、实时输入这些仍然属于客户端边界。RSC 的关键不是追新而是把数据读取和交互状态放在合适的位置。一、先区分数据组件和交互组件flowchart TD A[Component] -- B[Data Fetching] A -- C[Interactive State] B -- D[Server Component] C -- E[Client Component]展示型、数据读取型组件适合服务端按钮交互、表单状态、拖拽、动画控制更适合客户端。判断标准可以细化为一个决策树这个组件需要什么 ├── 只读取数据并渲染 → Server Component ├── 需要 useState / useEffect → Client Component ├── 需要 onClick / onScroll / 其他事件 → Client Component ├── 需要 browser API (localStorage, window, navigator) → Client Component ├── 大部分是 Server只有一个小交互 → 拆成 Container (Server) Island (Client) └── 不确定 → 先写成 Client后续优化一个常见误区是把需要动态数据的组件全都写成 Client Component。实际上服务端组件完全可以 async 读取数据库或 API// Server Component — 直接读数据库 export default async function UserProfile({ userId }: { userId: string }) { const user await db.user.findUnique({ where: { id: userId } }); if (!user) return NotFound /; return ProfileCard user{user} /; }但如果ProfileCard内部有编辑按钮、弹窗、表单那它应该是 Client Component接收 user 数据作为 props。二、服务端组件适合读数据export default async function ProductList() { const products await getProducts(); return ProductGrid products{products} /; }这种组件不需要把请求逻辑和数据依赖打进浏览器。用户拿到的是渲染结果客户端 JS 更少。更进一步的实践是组合 Server Component Client Islands// page.tsx — Server Component export default async function ProductPage({ params }: { params: { id: string } }) { const product await getProduct(params.id); const reviews await getReviews(params.id); return ( div ProductDetail product{product} / AddToCartButton productId{product.id} price{product.price} / ReviewList reviews{reviews} / /div ); }ProductDetail和ReviewList是 Server Component直接渲染数据。AddToCartButton是 Client Componentuse client负责加购交互。页面框架是 Server交互是 Island。这种模式的最大收益不是包体而是数据请求的简化。Server Component 可以直接 await 多个数据源不需要在前端写useEffectfetchloading/error的状态管理。数据怎么来、怎么组合全在服务端决定前端只负责渲染。踩坑Server Component 中 async 读取数据虽然优雅但要注意数据库连接的释放时机。如果getProduct内部使用了连接池但没有在请求结束后归还连接多个并行请求可能导致连接池耗尽。尤其是Promise.all([getProduct(id), getReviews(id)])这种并行请求每个 async component 函数内部维护自己的连接获取逻辑容易产生连接泄漏但看不出报错的问题。// 推荐在 Server Component 的数据获取函数中明确管理连接生命周期 export default async function ProductPage({ params }: { params: { id: string } }) { // 一次查询获取所有需要的数据减少连接占用 const { product, reviews } await getProductWithReviews(params.id); return ( div ProductDetail product{product} / AddToCartButton productId{product.id} price{product.price} / ReviewList reviews{reviews} / /div ); }还有一个容易被忽略的场景SEO 依赖的内容。搜索引擎爬虫可以执行 JS但渲染依赖不稳定的客户端数据会降低收录率。把 SEO 关键内容放在 Server Component 中返回静态 HTML搜索引擎能稳定索引。三、客户端组件负责交互use client; export function SearchBox() { const [keyword, setKeyword] useState(); return input value{keyword} onChange{e setKeyword(e.target.value)} /; }一旦组件需要useState、useEffect、浏览器事件就要成为客户端组件。不要为了服务端优先把交互写得别扭。场景描述我们曾试图把搜索页的SearchBox和SearchResults都写成 Server Component搜索触发时通过 URL 参数传递关键词刷新页面来展示结果。技术上可行但用户输入过程中的即时建议、防抖、历史搜索等功能全部丧失。更糟糕的是每次搜索都是一次完整的页面刷新——在移动端网络下这个体验等同于回到了 PHP 时代。教训即时交互组件不要去服务端。SearchBox应该保持 use client但SearchResults可以拆成两半搜索结果列表部分如果是基于关键词的纯数据展示可以放在 Server Component 中通过 URL 参数接收但搜索过程中的骨架屏、加载态、错误重试等仍需要客户端处理。关键不是把所有东西放一个端而是每个组件找到最适合它的端。四、边界要避免频繁穿越服务端和客户端组件混用时props 必须可序列化。函数、复杂实例不能随便传。rsc_boundary_check: serializable_props: required no_browser_api_in_server: true client_component_minimized: true如果一个页面边界切得太碎理解成本会上升。RSC 应该让结构更清楚不是更绕。Props 序列化是 RSC 核心约束之一。Server Component 传递给 Client Component 的 props 必须是可 JSON 序列化的。不能传函数、类实例、Symbol、BigInt。场景描述我们在一个商品详情页中Server Component 读取了商品数据后想直接传递product.calculateDiscount()方法给AddToCartButton——这是一个类方法JSON 序列化时会丢失。结果AddToCartButton中调用discount()时得到一个 undefined但没有任何控制台报错或编译期提示因为 TypeScript 把它的类型声明为了() number实际上运行时它根本不存在。// 踩坑Server Component 传递了不可序列化的方法 ProductCard product{product} / // product 是 class instance // 传输后 product.calculateDiscount 丢失 // 正确将计算结果作为普通值传递 const discountPrice product.calculateDiscount(); ProductCard productData{{ name, price, discountPrice }} /这个问题的排查成本很高——它不会在开发阶段报错只会在生产环境的 hydration 失败或功能异常中暴露。因此建议在项目中配置 ESLint 规则禁止 Server Component 直接向 Client Component 传递非可序列化的 props 类型。数据缓存也要一起考虑。服务端组件读取数据时要明确是静态、按请求还是按标签失效。否则页面看起来迁到了服务端实际缓存策略却不清楚。rsc_data_policy: static_content: cache user_dashboard: no_store_or_session_cache product_list: revalidate_by_tagRSC 的性能收益常常来自少发 JS 和合理缓存。只迁组件不设计数据策略收益会打折。踩坑我们上线 RSC 版本的 Dashboard 后发现首页加载速度反而变慢了。排查发现Dashboard 页面并行调用了 12 个数据源的fetch但 Next.js 默认的fetch缓存互相独立导致每次请求都触发数据库查询。改用 React 的cache()包装去重后相同请求的数据只查一次响应时间从 2.3 秒降到了 800ms。// 使用 React cache() 去重请求 import { cache } from react; const getUser cache(async (id: string) { return db.user.findUnique({ where: { id } }); }); // 同一请求周期内多次调用 getUser(123) 只会执行一次数据库查询五、总结React Server Components 适合数据读取和减少客户端包体但交互状态、浏览器 API 和即时反馈仍应留在客户端组件。不是所有组件都该搬到服务端。边界清楚RSC 才能提升体验而不是增加心智负担。评估时可以看客户端包体、首屏数据等待、交互延迟和代码复杂度。四个指标一起看比单纯追逐新架构稳得多。迁移也应该从边界清楚的页面开始。比如文档页、商品详情、只读报表通常比复杂编辑器更适合先试。先用低风险页面验证构建、缓存和部署链路再考虑核心交互页面。rsc_migration_order: docs_page product_detail dashboard_readonly complex_editor_last架构升级不要一上来挑战最复杂页面容易把技术评估变成事故演练。