OpenMontage Agent Skill 实战:避免 React Server Components Props 中的重复序列化
【免费下载链接】OpenMontageWorld's first open-source, agentic video production system. 12 production pipelines, 100+ tools, 700+ agent skill and production-knowledge files. Turn your AI coding assistant into a full video production studio.项目地址: https://gitcode.com/GitHub_Trending/op/OpenMontage
本文围绕 OpenMontage 仓库内置的 Vercel React 最佳实践技能中的一条具体规则——server-dedup-props(避免 RSC Props 中的重复序列化)展开,系统讲解 React Server Components 向客户端传输 props 时"按引用去重"的底层机制、哪些操作会破坏去重、嵌套数据的影响差异,以及如何把数据转换逻辑从服务端挪到客户端来缩减网络载荷。读完本文,你可以在评审或编写 Next.js / RSC 代码时准确识别重复序列化的反模式,并掌握一条可复制、可对照检查的优化策略。
规则定位:在 Vercel React 最佳实践体系中的位置
该规则的源文件位于 server-dedup-props.md,是 OpenMontage 仓库为 AI 编码助手(Agent)预置的一条"可加载"性能规则。从文件头部的 frontmatter 可以直接看到它在体系中的坐标:
--- title: Avoid Duplicate Serialization in RSC Props impact: LOW impactDescription: reduces network payload by avoiding duplicate serialization tags: server, rsc, serialization, props, client-components ---结合仓库中的技能组织文档可以确认它的归属与优先级:
- 按 SKILL.md 的分类表,
server-前缀对应第 3 类"Server-Side Performance",整体影响级别为 HIGH;server-dedup-props在该类中的描述是 "Avoid duplicate serialization in RSC props"。 - 按 _sections.md,第 3 节 Server-Side Performance 的定位是"优化服务端渲染与数据获取,消除服务端瀑布并缩短响应时间"。
- 按 README.md 的规则文件规范,文件名采用
area-description.md约定,章节由文件名前缀自动推断,影响级别分为 CRITICAL 到 LOW 共 6 档。本条规则自评 impact 为 LOW,属于"增量优化"档——它不改变正确性,而是通过避免重复序列化来减小网络载荷。 - 这套
rules/目录会被构建脚本编译进同目录的长文档 AGENTS.md,其中"避免 RSC Props 重复序列化"被编排为第 3.2 节。新规则的书写格式可参考 _template.md:frontmatter(title / impact / impactDescription / tags)+ 错误示例 + 正确示例。
换句话说,这条规则是"面向 Agent 与 LLM 的可执行知识":它要求代码生成与代码评审时,自动检查 props 中是否存在对同一数据的多次派生传递。
核心原理:RSC 序列化按"引用"而非"值"去重
规则给出的第一性原理只有两句话:
RSC→client serialization deduplicates by object reference, not value. Same reference = serialized once; new reference = serialized again. (RSC 到客户端的序列化按对象引用去重,而不是按值去重。同一引用只序列化一次;产生新引用就会被再次序列化。)
为什么这条原理如此重要?结合同目录下相邻规则 server-serialization.md 可以补全背景:React Server/Client 边界会把所有对象属性序列化成字符串,并嵌入 HTML 响应以及后续的 RSC 请求中,序列化数据直接决定页面体积与加载时间。也就是说,序列化成本是"按引用计数"的——如果你在两个 props 里传入了内容相同但引用不同的两份数据,即使值完全相等,它们也会被各序列化一遍,整份数据在网络上传输两次。
由此推出规则的核心操作准则:数据转换(.toSorted()、.filter()、.map()等)应放在客户端执行,而不是在服务端预处理好再传过去。
典型反模式与修复:从 6 个字符串降到 3 个
原文档给出的最小可复现示例如下。
错误写法(重复传输数组):
// RSC: sends 6 strings (2 arrays × 3 items) <ClientList usernames={usernames} usernamesOrdered={usernames.toSorted()} />这里usernames假设为 3 个元素的数组。usernames.toSorted()在原地排序的同时返回了一个新数组引用,于是序列化器看到的是"两个不同的数组",把 3 个字符串各发送两遍,共 6 个字符串。
正确写法(只发送 3 个字符串):
// RSC: send once <ClientList usernames={usernames} /> // Client: transform there 'use client' const sorted = useMemo(() => [...usernames].sort(), [usernames])要点有二:
- RSC 侧只传递原始数组一份,把"排序"这一派生逻辑整体下推到客户端;
- 客户端组件内部使用
useMemo派生排序结果,依赖项为usernames引用本身——由于 props 引用在两次渲染间保持稳定,useMemo的缓存同样有效。
嵌套去重行为:影响程度取决于数据类型
规则的第二个知识点是去重是递归生效的,但不同数据类型下的浪费程度差异很大:
string[]、number[]、boolean[]:影响为HIGH——数组本身 + 全部原始值(primitive)都会被完整复制一遍;object[]:影响为LOW——只有数组结构本身被重复,嵌套的对象仍会按引用去重。
原文档用两行代码直观演示了这种差异:
// string[] - duplicates everything usernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings // object[] - duplicates array structure only users={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4)解读第二行:users.toSorted()产生了一个新数组,所以"数组结构"被发了两份;但数组里的{id:1}、{id:2}这两个对象引用与原数组中完全相同,因此对象本身只序列化一次,最终是"2 个数组 + 2 个唯一对象",而不是 4 份对象。这解释了为什么该规则整体标为 LOW impact:对以对象为主的数据结构,浪费主要停留在稀疏的数组壳层上;而纯原始值数组则会把内容整个翻倍,属于需要优先清理的情况。
破坏去重的操作清单
以下操作都会创建新引用,从而打破 RSC 边界上的引用去重。评审代码时可以直接对照这张清单:
数组操作(产生新数组引用):
.toSorted().filter().map().slice()[...arr](展开语法)
对象操作(产生新对象引用):
{...obj}(对象展开)Object.assign()structuredClone()JSON.parse(JSON.stringify())
一个实用推论:.toSorted()这类 ES2023 不可变方法虽然避免了就地修改的副作用(仓库同一技能在 JavaScript Performance 一节还收录了js-tosorted-immutable规则讲不可变性本身),但它必然返回新引用——在 RSC 传参场景下,"不可变方法"与"去重破坏者"是同一件事的两面。
更多常见反模式与唯一例外
原文档还给了两类高频场景的对照示例:
// ❌ Bad <C users={users} active={users.filter(u => u.active)} /> <C product={product} productName={product.name} /> // ✅ Good <C users={users} /> <C product={product} /> // Do filtering/destructuring in client第一个例子中users.filter(...)生成了新数组引用,active与users各序列化一份(按上文分析,若users是对象数组,则对象本身不会翻倍,但数组结构与筛选结果仍重复);正确做法是只传users,把过滤逻辑写进客户端组件。第二个例子更隐蔽:product.name看似只是一个字符串,但"把对象的字段单独拆出来作为另一个 prop 传递"同样构成冗余——客户端拿到product后完全可以自行取product.name。这提示一个通用判断标准:凡是"客户端可以从已传递数据中派生出来"的值,都不应再单独作为 prop 发送。
规则同时声明了唯一例外:
Exception:Pass derived data when transformation is expensive or client doesn't need original. (当转换开销很大、或客户端根本不需要原始数据时,才应该传递派生后的数据。)
即:如果排序/过滤在服务端计算成本显著(例如超大数据集上的重计算),或者客户端组件只消费派生结果而完全用不到原数据(此时传原数据反而是浪费),则应在服务端完成转换、只传最终派生值。这一例外与相邻规则 server-serialization.md 的"只传客户端真正用到的字段"原则正好互补——前者解决"少传字段",本规则解决"别传两份",例外条款则划出"何时该传派生值"的边界。
实践落点:如何把这条规则用起来
结合仓库中该技能的组织方式,这条规则的实际使用路径很清晰:
- 代码评审对照:在 Next.js / RSC 项目的 Server 组件中,逐个检查传给
'use client'组件的 props——是否存在对同一数据的.toSorted()/.filter()/.map()/.slice()/ 展开派生、是否存在把嵌套字段重复拆出的"影子 props"(如product+productName); - 按数据类型评估收益:纯原始值数组的重复传递优先修复(HIGH 浪费);对象数组的重复可以排后(LOW 浪费,仅数组结构重复);
- 修复模式固定化:把派生逻辑移入客户端组件,用
useMemo包裹,依赖项写 props 引用本身; - Agent 工作流集成:该技能目录(SKILL.md 声明的触发时机是"编写、评审或重构 React/Next.js 代码时")中的每条规则文件都遵循统一的"错误示例 + 正确示例 + 例外说明"结构(见 _template.md),这种结构正是为了让 LLM 能稳定地做模式匹配式的自动重构。编译产物 AGENTS.md 则提供了全部 8 个类别的完整规则索引,便于在性能优化任务中整体加载。
需要说明的适用前提:本规则讨论的是 React Server Components 向客户端组件传 props 的序列化路径,仅在 RSC 架构(如 Next.js App Router)下生效;对纯客户端 SPA 或传统 SSR,该"按引用去重"的边界并不存在,规则不适用。同时 impact 为 LOW 的定位意味着它应作为性能清单中的收尾项——在消除瀑布(async 类,CRITICAL)与缩减 bundle(bundle 类,CRITICAL)之后,再系统性清理重复序列化,以获得载荷上的增量收益。
【免费下载链接】OpenMontageWorld's first open-source, agentic video production system. 12 production pipelines, 100+ tools, 700+ agent skill and production-knowledge files. Turn your AI coding assistant into a full video production studio.项目地址: https://gitcode.com/GitHub_Trending/op/OpenMontage
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考