Cherry Studio 的 React 性能规则 js-min-max-loop:用 O(n) 单次循环替代排序求最值
【免费下载链接】cherry-studio🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio
本篇围绕 Cherry Studio 仓库内置的 Agent 性能规则文件 js-min-max-loop.md 展开,讲清“只取最小值/最大值时为什么不该先排序”这一 JavaScript 数组性能准则:从复杂度推导、可复制的正误代码对照,到Math.min/Math.max展开运算符的浏览器上限,帮助你在写代码、做代码评审和重构 React/Electron 渲染层逻辑时,直接替换掉“为了取一个值而整表排序”的浪费写法。
这条规则在 Cherry Studio 规则体系中的位置
Cherry Studio 在 .agents/skills/vercel-react-best-practices 下内置了一套来自 Vercel Engineering 的 React/Next.js 性能优化规则集,共 62 条规则、8 个分类,按影响程度(impact)排定优先级。SKILL.md 中的优先级总表如下:
| 优先级 | 分类 | 影响级别 | 前缀 |
|---|---|---|---|
| 1 | 消除瀑布式等待 Eliminating Waterfalls | CRITICAL | async- |
| 2 | 打包体积优化 Bundle Size | CRITICAL | bundle- |
| 3 | 服务端性能 Server-Side | HIGH | server- |
| 4 | 客户端数据获取 Client-Side | MEDIUM-HIGH | client- |
| 5 | 重渲染优化 Re-render | MEDIUM | rerender- |
| 6 | 渲染性能 Rendering | MEDIUM | rendering- |
| 7 | JavaScript 性能 JavaScript Performance | LOW-MEDIUM | js- |
| 8 | 高级模式 Advanced Patterns | LOW | advanced- |
js-min-max-loop属于第 7 类 JavaScript 性能规则(前缀js-),该分类共 13 条,与它并列的还有js-early-exit(尽早返回)、js-combine-iterations(合并多次遍历)、js-set-map-lookups(用 Set/Map 做 O(1) 查找)、js-tosorted-immutable(用toSorted()保持不可变)等。
规则文件的元数据由 frontmatter 声明,js-min-max-loop.md的头部信息为:
--- title: Use Loop for Min/Max Instead of Sort impact: LOW impactDescription: O(n) instead of O(n log n) tags: javascript, arrays, performance, sorting, algorithms ---其中impact: LOW表示该规则的收益属于增量优化(按 README.md 定义的 Impact Levels:CRITICAL 到 LOW 六级),impactDescription直接点明收益本质——把 O(n log n) 降到 O(n)。这类规则面向 Agent 和 LLM 自动评审/重构场景,所有规则文件可编译进 AGENTS.md 作为完整参考文档。
问题本质:排序取首元素是过度设计
规则的核心论断只有一句话:寻找最小或最大元素只需要对数组做一次线性遍历,排序既浪费又慢。
典型的错误写法是把整个数组复制一份、排序,然后取第一个元素:
interface Project { id: string name: string updatedAt: number } function getLatestProject(projects: Project[]) { const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt) return sorted[0] }这里发生了三件多余的事:
[...projects]复制了整个数组——产生一次 O(n) 的内存分配;.sort()对整个数组排序——V8 等引擎使用 TimSort 一类稳定排序,平均复杂度 O(n log n),且需要 O(n) 额外空间;- 最后只用到了
sorted[0]——其余 n−1 个元素的相对顺序全部是白排的。
“同时取最旧和最新”的变体同样有这个缺陷:
function getOldestAndNewest(projects: Project[]) { const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt) return { oldest: sorted[0], newest: sorted[sorted.length - 1] } }表面上“一次排序拿到两个值”似乎更划算,但从算法角度看,当目标只是 min/max 时,排序仍然是不必要的 O(n log n)。数组规模越大差距越明显:n = 10 000 时 O(n log n) 约需 13 万量级的比较,而 O(n) 只需 1 万次比较,且没有复制与排序的临时内存开销。
正确写法:单次循环求最值(O(n))
规则给出的正确实现是“单趟遍历 + 边界判空”,无复制、无排序:
function getLatestProject(projects: Project[]) { if (projects.length === 0) return null let latest = projects[0] for (let i = 1; i < projects.length; i++) { if (projects[i].updatedAt > latest.updatedAt) { latest = projects[i] } } return latest } function getOldestAndNewest(projects: Project[]) { if (projects.length === 0) return { oldest: null, newest: null } let oldest = projects[0] let newest = projects[0] for (let i = 1; i < projects.length; i++) { if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i] if (projects[i].updatedAt > newest.updatedAt) newest = projects[i] } return { oldest, newest } }实现要点逐条拆解:
- 空数组短路:函数入口先判断
projects.length === 0,返回null(或双空结构),避免对空数组取projects[0]得到undefined后继续比较抛出异常; - 以
projects[0]作为初始候选:不需要额外的-Infinity/Infinity哨兵或类型断言,候选值始终是数组中的真实元素,返回值类型保持Project而非数值; - 从索引 1 开始比较:元素 0 已经是候选,跳过可省一次比较;
- 最值同趟获取:
getOldestAndNewest在一次循环里同时维护oldest与newest两个候选,两个比较互不依赖,总比较次数至多 2(n−1),仍然是 O(n); - 不修改原数组:循环只读不写,天然满足 React 数据流的不可变约束(与同分类的 js-tosorted-immutable.md 关注的不可变性是一脉相承的)。
从源码结构看,这套模式之所以被单独立为一条规则而不是散落在泛泛的“优化建议”里,正是因为它在业务代码中极其常见:按时间戳取最新会话、按高度/宽度取边界值、按分数取极值等场景,都容易顺手写出“先 sort 再取 [0]”的直觉代码。
替代方案:Math.min / Math.max 的适用边界
对于纯数值小数组,规则也认可了更简的展开运算符写法:
const numbers = [5, 2, 8, 1, 9] const min = Math.min(...numbers) const max = Math.max(...numbers)但文档明确警告其上限:...spread会把数组元素展开成函数参数,受限于引擎调用栈的参数上限——数组过长时不仅更慢,还可能直接抛出错误。规则中给出的实测参考值是:Chrome 143 中数组长度上限约 124000,Safari 18 约 638000,具体数值随浏览器版本变化。因此结论是:
- 小数组:
Math.min(...arr)/Math.max(...arr)可读性好,可以使用; - 大数组或长度不可控的数组(例如列表数据、导入的表格行、搜索结果集):一律用循环写法,避免触碰展开上限;
- 对象数组(如
Project[]):Math.min/max只接受数值,无法直接比较updatedAt字段,循环(或先map出数值再循环)是唯一可靠选择。
仓库中的实际印证:Math.min/Math.max 的普遍用法
规则虽然以Project时间戳为例,但它面向的正是像 Cherry Studio 这类 Electron + React 桌面应用的渲染层代码。在 Cherry Studio 的 renderer 侧,Math.min/Math.max已被大量用于“边界钳制”类场景,例如:
- src/renderer/components/QuickPanel/heights.ts:计算快捷面板的布局高度时使用上下界钳制;
- src/renderer/components/FilePreview/plugins/spreadsheet/XlsxGrid.tsx:电子表格预览的行列尺寸计算;
- src/renderer/components/Chart/charts/buildCartesianOption.ts、src/renderer/components/ModelSelector/ModelSelector.tsx 等组件中的尺寸/数值边界处理。
这些位置处理的多是数值数组(坐标、像素尺寸、行列数),且规模有限,使用Math.min/Math.max完全在规则认可的安全区间内。而当数据来自用户导入、文件解析或远程返回、长度不可控时,就应改为本规则的单循环写法——这正是“小数组用 Math、大数组用循环”边界的真实应用场景。
适用前提与限制
- 影响级别 LOW:这条规则属于增量优化,不是 CRITICAL 级的“瀑布消除”或“打包瘦身”;当数组本身只有几到几十项时,排序与循环的性能差异在体感上不可察觉,此时可优先关注可读性。规则的价值体现在长度不可控或数据量大的路径上;
- 仅适用于“只需要最值”的场景:如果你确实需要返回有序列表,或需要 Top-K(K 较大)元素,排序/堆等其他手段才是正确工具,本规则不适用;
- 比较器一致性:循环写法直接比较的是你选定的键(示例中为
updatedAt),不会出现sort默认按字典序排序导致字符串数字比较出错的经典坑; - 规则维护流程:该规则集通过 README.md 描述的
pnpm build编译流程生成AGENTS.md与测试用例,规则文件按文件名前缀自动归入分类、按标题排序,新增规则只需遵循_template.md的 frontmatter + 正误对照结构。
小结
js-min-max-loop这条规则给出的模式可以浓缩为一句话:只需要最小/最大值时,用一次带空值短路的线性扫描(O(n)),不要用“复制 + 排序 + 取首/尾”(O(n log n));小数值数组可用Math.min/Math.max,但大数组受展开运算符栈深限制,应坚持循环写法。在 Cherry Studio 内置的 62 条性能规则体系中,它属于第 7 类 JavaScript 性能准则,是代码评审与 Agent 自动重构时可直接套用的判定项:看到“sort 之后只取[0]或length - 1”,即可替换为上文给出的单循环实现。
【免费下载链接】cherry-studio🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考