首页 / 知识中心 / 技术分享
技术分享

Next.js Server Components 实战:边界该划在哪

Next.js Server Components 实战:边界该划在哪

RSC 的核心不是"更快",而是把渲染和数据获取搬回服务端。真正决定项目成败的是 'use client' 划在哪里——划错一处,整棵子树都会退回客户端。

先说结论:Server Components 带来的不是"性能开关",而是一次职责重划:默认在服务端渲染,只在需要交互的最小范围标 'use client'。最容易踩的坑也正是这个指令——它在模块图上是一道"污染边界",一旦标在靠近根组件的位置,整棵子树连同其依赖都会被打包进客户端。所以实战中真正要练的不是写 RSC,而是判断边界该切在哪。

一、先理解 'use client' 到底做了什么

很多人以为 'use client' 是"这个组件在客户端渲染"。不准确。它的真实语义是:这个模块及其导入的所有模块,属于客户端包。

这意味着两个后果:

  • 被标为 client 的组件,其所有子组件(无论有没有标记)都会进入客户端包
  • 它导入的任何模块——工具函数、UI 库、甚至是只在初始化时用一次的大依赖——也会被打包进去

所以一个常见的性能事故是:为了给页面加一个下拉菜单,把 'use client' 标在了布局组件上,结果整页的依赖全部进了客户端 bundle。体积涨了,但没人知道为什么。

二、边界应该切在叶子,不是根

原则只有一条:交互标记尽量下沉到真正需要状态的最小单元。

举个具体的例子。一个商品详情页需要:展示商品信息(静态)、切换规格(交互)、加入购物车(交互)、展示评价列表(静态)。

错误写法是把整个页面标成 client 组件,然后在里面 fetch 数据。这样服务端渲染的好处几乎全丢了。

正确写法是页面本身保持 Server Component,只在规格选择器和加购按钮上标 client:

// app/product/[id]/page.js  —— Server Component,不标 'use client'
export default async function Page({ params }) {
  const product = await getProduct(params.id); // 直接在服务端查库
  return (
    <main>
      <ProductInfo data={product} />      {/* 静态,服务端渲染 */}
      <SkuPicker skus={product.skus} />    {/* 交互,client */}
      <ReviewList productId={product.id} /> {/* 静态 */}
    </main>
  );
}

这样只有 SkuPicker 及其依赖进客户端包,商品数据和评价列表都在服务端就渲染成 HTML 了。

三、数据获取全部上移

RSC 里可以直接写 await,不需要 useEffect + loading 状态那一套。这不是语法糖,而是消除了"先渲染空壳、再请求数据、再重渲染"的三段式闪烁。

实践中的几条经验:

  • 能并行就并行:多个互不依赖的请求用 Promise.all,不要串行 await
  • 请求去重:同一个请求在多个组件里调用不用担心,Next 会自动去重(基于 fetch 缓存)
  • 敏感信息留在服务端:数据库连接串、API 密钥可以直接在 Server Component 里用,不会进入客户端包。这是 RSC 相对 CSR 最实际的安全收益

四、序列化边界:不能传函数

Server Component 传给 Client Component 的 props 必须可序列化。这意味着不能传函数、不能传 class 实例、不能传 Date 对象的某些用法(不同版本行为有差异,建议统一转成字符串)。

想把"点击后做什么"传给客户端组件,正确的做法是传数据 + 用 Server Actions,而不是传回调函数。这是从 React 传统写法迁移过来时最容易出错的地方。

五、缓存与重新验证

Next 14 里 fetch 默认会被缓存,这个默认值踩过很多人。三档控制:

写法含义适用
cache: 'force-cache'永久缓存,除非手动 revalidate极少变动的配置、字典
next: { revalidate: 60 }60 秒内用缓存列表页、文章页
cache: 'no-store'每次都重新请求用户中心、订单、库存

还有一个容易忽略的点:revalidatePath 要在数据变更之后调用,而且路径要写对。写错了不会报错,只是页面上永远是旧数据,排查起来很费时间。

六、什么时候不该用 RSC

RSC 不是万能方案。以下几种情况用传统方案更省事:

  • 纯内部后台:不需要 SEO,用户量有限,SPA 的开发体验更顺手
  • 重度客户端交互:富文本编辑器、白板、地图编辑器这类,几乎所有组件都需要状态
  • 团队不熟悉服务端渲染:RSC 的心智负担确实比 CSR 高,强行上会拖慢交付

七、一份可执行的检查清单

  • 根布局、页面组件不标 'use client'
  • 所有 'use client' 都在叶子组件上,且能用 grep 数出来有几个
  • 没有在 client 组件里直接查数据库或读环境变量
  • 传递的 props 都是可序列化的
  • 列表类页面的 fetch 有明确的 revalidate 策略,不是默认值
  • 构建产物里检查客户端 bundle 体积,确认没有意外引入大依赖

最后一条最实用:与其在架构图上争论边界,不如直接看构建产物的体积变化。数字会告诉你边界划错在哪。

携手微众,共创未来

有新项目,或想找长期技术伙伴,都可以直接聊。

周一至周五 9:00-18:00 即时响应

微信客服二维码

添加微信客服,1 对 1 沟通