Cherry Studio 中的数组比较优化先做长度检查再跑昂贵比较Vercel React 最佳实践 js-length-check-first 规则解析【免费下载链接】cherry-studio Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio在 React 与 Electron 渲染层中判断两个数组是否相等是高频操作——表单脏检查、引用源比对、流式渲染去重都离不开它。本文围绕 Cherry Studio 仓库内置的 Vercel React 性能最佳实践规则js-length-check-first数组比较前先做 O(1) 长度检查展开讲清该规则的原理、正确写法与四条收益并结合本仓库渲染进程中的真实实现如 Agent 配置脏检查、消息引用源比对说明该模式在热点路径上的落地方式帮助你在编写或评审 React 代码时避免排序/深比较永远全量执行的性能陷阱。规则定位它在 Vercel React 性能规则体系中的位置Cherry Studio 仓库将 Vercel Engineering 的 React/Next.js 性能优化指南内置为 Agent 技能位于 .agents/skills/vercel-react-best-practices/ 目录。该技能按影响力impact将规则分为 8 个类别完整索引见 SKILL.md 与 AGENTS.md优先级类别影响力前缀1消除串行瀑布流WaterfallsCRITICALasync-2包体积优化CRITICALbundle-3服务端性能HIGHserver-4客户端数据获取MEDIUM-HIGHclient-5重渲染优化MEDIUMrerender-6渲染性能MEDIUMrendering-7JavaScript 性能LOW-MEDIUMjs-8高级模式LOWadvanced-本文主角js-length-check-first属于第 7 类 JavaScript 性能规则frontmatter 中声明其影响力为MEDIUM-HIGH理由是影响面描述avoids expensive operations when lengths differ在长度不同时跳过昂贵操作。在完整合并文档 AGENTS.md 中它对应章节7.7 Early Length Check for Array Comparisons。核心原则长度不等即不相等规则原文只有一句话的核心当你要用昂贵操作排序、深比较、序列化来比较两个数组时先检查长度。长度不同数组就不可能相等。在真实应用中这条规则的价值在比较发生在热点路径时被放大——典型如事件处理器、渲染循环render loops中每次渲染都要执行的脏检查/变更检测。错误写法每次都执行昂贵比较规则文件 给出的反例是function hasChanges(current: string[], original: string[]) { // 无论长度是否相同都会排序并 join return current.sort().join() ! original.sort().join() }这个写法有三个问题排序永远执行即使current.length是 5、original.length是 100两个 O(n log n) 的排序也会完整跑完之后还要拼接字符串、做字符串比较纯纯的浪费产生大量临时内存join()会为两个数组各生成一个完整的拼接字符串对大数组而言这是可观的内存与 GC 压力sort()会原地修改数组Array.prototype.sort()是 in-place 排序会直接改变传入的current和original。在 React 场景下这意味着 props/state 被意外篡改破坏 React 的不可变数据模型可能引发陈旧闭包 bug 与难以追踪的渲染异常。正确写法O(1) 长度检查 不可变排序 提前返回规则给出的正例function hasChanges(current: string[], original: string[]) { // 长度不同则提前返回O(1) if (current.length ! original.length) { return true } // 只有长度相同才排序 const currentSorted current.toSorted() const originalSorted original.toSorted() for (let i 0; i currentSorted.length; i) { if (currentSorted[i] ! originalSorted[i]) { return true } } return false }逐点拆解这段代码它同时应用了技能库中三条相邻规则长度前置检查本规则current.length ! original.length是 O(1) 判断。一旦命中立即得出不相等的结论后面的排序、循环全部跳过toSorted()替代sort()对应规则 js-tosorted-immutabletoSorted()返回一个排序后的新数组不修改原数组保持 props/state 的不可变性。该规则文件还给出了旧环境的回退方案——const sorted [...items].sort(...)toSorted()在现代环境普遍可用Chrome 110、Safari 16、Firefox 115、Node.js 20逐元素比较时提前返回与规则 js-early-exit 同构用索引循环而非join()拼串比较发现第一个不同的元素立即return true不必比完剩余所有元素也避免了两个大字符串的分配。四条收益总结继承原文档相比永远排序 join的写法新写法更高效的完整理由长度不同时完全跳过排序与拼接的开销不再为拼接字符串消耗内存大数组尤其重要不再原地修改传入的原始数组React props/state 安全发现第一个差异即提前返回平均情况下的比较成本更低。复杂度视角什么时候值得做长度前置检查从复杂度上可以量化这个优化设两数组长度为 n 与 m。错误写法的最坏成本是O(n log n m log m)排序 O(n m)拼接与比较且无条件执行正确写法在最常见情况长度不同例如流式输入中消息 parts 不断增删只需O(1)即使长度相同正确写法在首个不同元素处即可返回最坏仍为 O(n log n)但不再分配拼接字符串。因此该优化的收益与长度不同的频率正相关。文档特别指出当比较运行在热点路径事件处理器、渲染循环时这个 MEDIUM-HIGH 级别的优化尤为值钱——因为在渲染循环中这段代码每次渲染都会执行任何常数级的浪费都会被帧频放大。Cherry Studio 仓库中的同构实践该规则并非纸上谈兵。Cherry Studio 渲染进程的源码里多处脏检查/等价性判断正是先比长度或 Set size再逐元素比较并提前返回的标准形态可以作为该规则的一手印证。实践一Agent 配置 DTO 的数组等价判断agentForm.ts 在生成 Agent 配置的增量补丁dirty check时用两个工具函数判断数组是否变化function arraysEqual(a: readonly string[], b: readonly string[]): boolean { if (a.length ! b.length) return false for (let i 0; i a.length; i) if (a[i] ! b[i]) return false return true } function stringSetsEqual(a: readonly string[], b: readonly string[]): boolean { const aSet new Set(a) const bSet new Set(b) return aSet.size bSet.size [...aSet].every((value) bSet.has(value)) }arraysEqual严格遵循本文规则第一行就是 O(1) 长度检查长度不等直接返回false之后才进入逐元素循环顺序敏感的比较stringSetsEqual则处理顺序不敏感的集合语义先构造Set先用size做 O(1) 前置检查再借助 Set 的 O(1) 查找做成员判断。这里还叠加了技能库中的另一条规则 js-set-map-lookups用 Set/Map 做 O(1) 查找避免includes的 O(n) 扫描。同目录的 assistantForm.ts 中也有同样的if (a.length ! b.length) return false模式说明这是本仓库表单脏检查的通用约定。实践二流式消息引用源的注册表比对citations.ts 的isSameRegistry用于判断引用可用的消息片段是否变化从而决定是否让流式文本 chunk 使所有消息的 parts 切片失效function isSameRegistry( previous: CitationPartsRegistry, parts: readonly CherryMessagePart[], prefixCountByMessageId: ReadonlyMapstring, number ): boolean { if (previous.parts.length ! parts.length) return false if (previous.prefixCountByMessageId.size ! prefixCountByMessageId.size) return false if (parts.some((part, index) previous.parts[index] ! part)) return false for (const [messageId, count] of prefixCountByMessageId) { if (previous.prefixCountByMessageId.get(messageId) ! count) return false } return true }这是规则原文所说热点路径的典型场景流式输出时每次收到文本 chunk 都可能触发一次该比较。代码第一、二行分别对数组长度和 Map size 做 O(1) 前置检查全部命中相等之后才做逐元素引用比较利用Object.is级别的引用相等成本远低于深比较。源文件中的注释也说明了设计意图Returnspreviouswhen nothing citable changed …, so a streaming text chunk never invalidates every messages prior-parts slice.——即通过廉价的等价性检查避免流式更新造成全量重计算。仓库内对toSorted()的使用正确示例依赖的不可变排序 API 在本仓库同样广泛使用例如 composerDraft.ts、useAgentSessionStreamStatuses.ts、Topics.tsx 等渲染进程文件均调用了toSorted()印证了 js-tosorted-immutable 规则.sort()原地修改数组在 React 状态与 props 中会引发 bug应使用.toSorted()创建新数组在本项目渲染层的落地。适用边界与常见变体将该模式推广到日常代码时有几个适用前提与变体值得注意前提比较语义是相等/不变判定。规则依赖的数学事实是长度不等 ⇒ 不相等所以它适用于相等性判断与变更检测dirty check。如果你要的是求两个数组的交集/差异明细长度检查只能作为快路径不能替代完整算法顺序敏感 vs 顺序不敏感顺序敏感如消息 parts、配置字段列表长度检查 逐元素!循环即可时间复杂度 O(n)连排序都可省本仓库arraysEqual就是这种形态顺序不敏感如标签集合、已启用技能 ID 列表长度/size 检查 Set成员判断对应stringSetsEqual与 agentForm.ts 中的 diffSkillUpdates用双向 Set 差集计算启用/禁用增量只有确实需要排序后对齐比较时才付出 O(n log n) 的排序成本且必须用toSorted()保持不可变不要为了比较而序列化JSON.stringify(a) JSON.stringify(b)是昂贵操作的典型代表全量序列化 字符串比较 键序敏感本规则同样适用于用它做缓存键前的前置长度检查与useMemo/useCallback依赖项的配合React 中大量比较发生在 effect 依赖与 memo 比较函数里。在自定义比较函数如areEqual里先做长度检查可以让 memo 组件在绝大多数渲染中以 O(1) 短路把节省的 CPU 留给真正需要重算的帧。小结js-length-check-first是一条MEDIUM-HIGH 影响力、写起来只需一行的规则在任何昂贵数组比较排序、深比较、序列化之前先用 O(1) 的长度检查做前置判断长度不同直接得出不相等。它与toSorted()不可变排序、Set/Map O(1) 查找、提前返回这几条规则天然组合共同构成了渲染热点路径中变更检测的标准写法。Cherry Studio 仓库中 agentForm.ts 的arraysEqual/stringSetsEqual与 citations.ts 的isSameRegistry展示了该模式在表单脏检查与流式引用去重中的真实用法完整规则集与其余 61 条规则可在 SKILL.md 的类别索引和 AGENTS.md 的合并文档中继续查阅。【免费下载链接】cherry-studio Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考