跳到主内容
← 全部文章

2026 年 8 月 22 日2 分钟

搞懂 React Server Components:一次讲清楚边界在哪

#React#Next.js

目录

第一次用 App Router 的时候,我踩了个很典型的坑:在一个 Server Component 里给子组件传了个函数当 prop,然后浏览器控制台给我报了一句话——

Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".

当时我只觉得规则很烦。后来想明白了:RSC 的核心不是「服务端渲染」,而是序列化边界

边界才是重点

服务端组件渲染的结果是一段可以被序列化的数据流(RSC Payload),它要通过网络传给客户端。能穿过这条边界的只有:

  • 普通对象、数组、字符串、数字、布尔、null
  • Date、Map、Set、TypedArray
  • JSX 元素(本身也是可序列化的描述)
  • Promise(会被流式处理)
  • 标记为 "use server" 的 server action 引用

传不过去的是:函数、类实例、Symbol、以及任何带原型的自定义对象

// ❌ 这样不行
<ClientChart formatter={(v) => v.toFixed(2)} />
 
// ✅ 把格式化的事情留给客户端自己决定
<ClientChart precision={2} />

「use client」不是「这个组件在浏览器跑」

更准确的理解是:它是客户端的入口标记。打包时,构建工具从这个标记开始,把它 import 的所有模块一起打进客户端 bundle。

所以一个常见误区是:以为加了 "use client" 的文件就完全不在服务端执行。实际上客户端组件在 SSR 阶段依然会在服务端跑一遍产出 HTML,只是它不会在服务端「重新渲染」。

什么时候该切到客户端

判断标准其实只有一条:这个组件需不需要响应交互或访问浏览器 API

需要用到 useStateuseEffectonClickwindowlocalStorage —— 那就加 "use client"。否则,留在服务端。

一个实用技巧:把 "use client" 尽量往下推。不要给整个页面加,只给真正需要交互的那颗叶子节点加。父级保持服务端,能传给它的 JSX 会作为 children 原样透传,不占用客户端 bundle 体积。

// 页面是服务端组件
export default function Page() {
  return (
    <ThemeToggle>
      {/* 这棵子树依然是服务端渲染的 */}
      <ServerRenderedList />
    </ThemeToggle>
  )
}

小结

  • RSC 的本质是序列化边界,不是渲染位置
  • "use client" 标记的是打包入口,不是执行环境
  • 把客户端边界往下推,bundle 才会小

想清楚这三点,App Router 里 90% 的困惑会自己消失。

评论

0 / 500

无需登录,昵称和评论会公开显示。

评论加载中……