Cherry Studio 规则实践解读:合并多次数组遍历的 JS 性能优化(js-combine-iterations)
【免费下载链接】cherry-studio🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio
在 Cherry Studio 仓库的.agents/skills/目录下内置了一套源自 Vercel 工程实践的 React/Next.js 性能优化规则集,其中js-combine-iterations规则针对一个高频反模式:对同一个数组连续调用多次.filter()或.map(),导致数组被重复遍历。本文完整解析该规则的问题本质、正确写法与适用边界,并结合仓库中的技能定义文件(SKILL.md、README.md)与编译产物(AGENTS.md)说明这条规则在整套规则体系中的定位、编写规范与使用方式。
规则定位:JavaScript Performance 类别中的第 7.6 条
该规则的源文件是 js-combine-iterations.md,其 frontmatter 元数据如下:
| 字段 | 值 | 含义 |
|---|---|---|
title | Combine Multiple Array Iterations | 规则标题 |
impact | LOW-MEDIUM | 影响级别:低-中(属于渐进式收益) |
impactDescription | reduces iterations | 收益来源:减少遍历次数 |
tags | javascript, arrays, loops, performance | 检索标签:JS、数组、循环、性能 |
在整套技能中,规则按影响程度分为 8 个优先级类别(见 SKILL.md 的 "Rule Categories by Priority")。本规则属于第 7 类JavaScript Performance(前缀js-,整体影响级别 LOW-MEDIUM,优先级第 7),与消除瀑布流(async-,CRITICAL)、包体积优化(bundle-,CRITICAL)等高优先级类别相比,它处理的是纯 JS 层面的微优化。编译后的 AGENTS.md 目录将其编号为7.6 Combine Multiple Array Iterations,同章节还有 7.7 Early Length Check、7.10 flatMap 单遍转换等邻近规则。
规则详解:把 N 次遍历合并为 1 次
问题描述
原文规则的核心陈述只有两句:多个.filter()或.map()调用会多次遍历数组;应把它们合并进一个循环。
每一个链式或并列的数组高阶函数调用,都会对原数组完整走一遍迭代。当同一段代码针对同一数组发起多个筛选/映射请求时,遍历次数就是调用次数的线性叠加。以规则中的反例为例(三次独立遍历):
// 错误写法:同一个 users 数组被完整遍历 3 次 const admins = users.filter(u => u.isAdmin) const testers = users.filter(u => u.isTester) const inactive = users.filter(u => !u.isActive)三个结果集相互独立,但数据源是同一个users数组。运行时的实际开销是 3N 次元素访问,外加 3 个中间数组的分配。
正确写法:单次遍历 + 条件分桶
规则给出的正确写法是"单循环多目标"模式:先声明各个目标数组,在一次for...of遍历中根据条件把元素推入对应的桶:
// 正确写法:只遍历 1 次,一次循环产出 3 个结果集 const admins: User[] = [] const testers: User[] = [] const inactive: User[] = [] for (const user of users) { if (user.isAdmin) admins.push(user) if (user.isTester) testers.push(user) if (!user.isActive) inactive.push(user) }两个实现细节值得注意:
- 显式类型标注。合并后需要手工声明每个结果数组(
const admins: User[] = []),因为.filter()会自动推导类型,而空数组[]会被推断为never[],缺少标注在 TypeScript 下会直接报错。规则示例特意保留了这些标注,这正是合并写法需要付出的类型成本。 - 各分支相互独立。注意三个
if不是if/else if链——一个用户完全可能既是isAdmin又是!isActive,会被同时推入两个桶,语义与三个独立.filter()完全一致。若误用else if,就会改变原逻辑的语义。
收益与适用边界的分析
从复杂度看,两种写法都是 O(N),但常数项差异明确:
- 元素访问与条件求值次数从 3N 降到 N;
- 中间结果只保留最终需要的数组,反例中每个
.filter()各自产生的分配模式不变,但遍历调度(函数调用、闭包求值)开销减少两次。
该规则被定为LOW-MEDIUM影响级别,隐含的判断是:它属于"值得做但不是最优先"的优化。可以推断出实践上的适用边界:
- 值得做:数组规模大(成百上千以上)、处于热路径(渲染循环、高频事件处理、批量数据导入导出);
- 不值得做:数组只有个位数元素,或筛选条件之间逻辑复杂、合并后单个循环体变得难以审查——此时三次独立
.filter()的声明式可读性更优。
这一点也与整套规则的优先级设计一致:在 SKILL.md 中,消除瀑布流和包体积被列为 CRITICAL,而本条只排到第 7 优先级,提示开发者先解决结构性问题,再做循环层面的微调。
同族规则:JavaScript Performance 类别全景
本规则不是孤立的。按 SKILL.md 的快速参考表,第 7 类js-规则共 13 条,共同目标是减少 JS 执行中的冗余工作与查找开销:
| 规则文件 | 要点 |
|---|---|
js-batch-dom-css | 通过 class 或cssText批量提交 CSS 变更,避免样式重算 |
js-index-maps | 为重复查找构建索引 Map |
js-cache-property-access | 循环内缓存对象属性访问 |
js-cache-function-results | 用模块级 Map 缓存函数结果 |
js-cache-storage | 缓存 localStorage/sessionStorage 读取 |
js-combine-iterations | 合并多个 filter/map 为一次循环(本文规则) |
js-length-check-first | 昂贵比较前先检查数组长度 |
js-early-exit | 函数内提前返回 |
js-hoist-regexp | RegExp 创建提升到循环外 |
js-min-max-loop | 用循环求 min/max 代替排序 |
js-set-map-lookups | 用 Set/Map 实现 O(1) 查找 |
js-tosorted-immutable | 用toSorted()代替sort()保持不可变 |
js-flatmap-filter | 用 flatMap 单遍完成映射+过滤 |
其中最常与本规则配合判断的是 js-flatmap-filter.md。两条规则解决的是"多次遍历"这一同族问题的不同形态:
js-combine-iterations:多个结果集、每个结果集都是原数组的某种筛选 → 用单循环分桶;js-flatmap-filter:一个结果集,但需要先变换再过滤,.map().filter(Boolean)链会产生中间数组并遍历两次 → 用flatMap单遍完成。
// flatMap 规则的反例:2 次遍历 + 中间数组 const userNames = users .map(user => user.isActive ? user.name : null) .filter(Boolean) // flatMap 规则的正确写法:1 次遍历、无中间数组 const userNames = users.flatMap(user => user.isActive ? [user.name] : [] )选型口诀可以概括为:多桶分流出多个数组,走合并循环;变换后再过滤出一个数组,走flatMap。
规则文件的编写规范:frontmatter、影响级别与构建流程
这套技能的价值不仅在于单条规则,还在于它定义了机器可读的规则格式,使 Agent/LLM 可以批量检索和应用。了解这套规范有助于理解js-combine-iterations.md每个字段的用途。
按 README.md 的说明,规则库的目录结构为:
rules/— 单条规则文件(每条一个 md 文件);AGENTS.md— 由规则编译生成的完整文档(本仓库中即 AGENTS.md,含自动编号的目录与全部规则展开);test-cases.json— 为 LLM 评估提取的测试用例(生成物)。
每个规则文件必须遵循固定骨架:YAML frontmatter(title、impact、impactDescription、tags)+ 简短的问题陈述 +Incorrect 代码块(带错误原因标注)+Correct 代码块(带正确原因标注)+ 可选的补充说明。js-combine-iterations.md正是这一骨架的标准实例。
文件命名约定:文件名采用前缀-描述.md格式,前缀决定章节归属,例如js-对应 JavaScript Performance(第 7 节)、async-对应 Eliminating Waterfalls(第 1 节);下划线开头的文件(如_template.md)是特殊文件,不参与构建;章节内规则按标题字母序自动排序,编号(7.6 之类)在构建时自动生成,无需手工维护。
影响级别共 6 档,从高到低:CRITICAL(最大收益)、HIGH、MEDIUM-HIGH、MEDIUM、LOW-MEDIUM(低-中收益)、LOW(渐进式改进)。本规则取LOW-MEDIUM,并在impactDescription中补充了收益来源 "reduces iterations",这在编译产物中会呈现为 "Impact: LOW-MEDIUM (reduces iterations)"(见 AGENTS.md 7.6 节)。
构建与维护命令(由 README 定义,适用于按此规范新建的规则库):
pnpm install # 安装依赖 pnpm build # 从 rules/ 编译生成 AGENTS.md 与 test-cases.json pnpm validate # 校验所有规则文件 pnpm extract-tests # 提取 LLM 评估测试用例 pnpm dev # build + validate在 Cherry Studio 仓库中如何使用该技能
结合 SKILL.md 的 "When to Apply" 一节,该技能在 Cherry Studio 仓库中的定位是Agent/开发者的 React 代码编写参考,触发场景包括:
- 编写新的 React 组件或 Next.js 页面;
- 实现数据获取(客户端或服务端);
- 评审代码时排查性能问题;
- 重构既有 React 代码;
- 优化包体积或加载时间。
具体到js-combine-iterations这条规则,实际用法是:当在评审或重构 src/renderer/ 下的大量 TSX/TS 代码(聊天列表、资源目录、消息处理等含数组筛选逻辑的模块)时,如果发现同一数组被连续多次.filter()/.map(),即可按上述"单循环多目标"模式重写。技能要求每条规则文件给出"解释 + 错误示例 + 正确示例 + 上下文参考"四要素,目的正是让 Agent 能直接检索单条规则文件(rules/js-combine-iterations.md)并照搬模式,而不必读完全量文档。
此外,该技能的存在也被仓库自身的功能所索引:技能名称 "vercel-react-best-practices" 出现在技能搜索功能的测试夹具中(skillSearch.test.ts 及其 fixtures 目录下的 JSON 文件),说明仓库的技能发现机制会把它纳入可搜索范围。
落地检查清单
将本规则应用到一次实际重构时,可以按以下清单执行:
- 识别模式:同一段作用域内,是否对同一数组发起了 2 次及以上的
.filter()/.map(),且各次调用的数据源未变化; - 确认结果集形态:多个相互独立的筛选结果 → 单循环分桶;变换后过滤出单个结果 → 改用
flatMap(参考 js-flatmap-filter.md); - 保持语义:分桶分支用独立
if而非if/else if(除非原逻辑本就互斥);为每个结果数组补上显式类型标注; - 评估收益档位:确认数组规模与调用频率足以让 LOW-MEDIUM 级别的优化产生可感知收益,否则保留声明式写法以换取可读性;
- 回归验证:按仓库根 AGENTS.md 的开发约定,运行
pnpm lint(含 format + typecheck + i18n 检查)并执行覆盖改动模块的测试(如pnpm exec vitest run <文件>),确认行为一致。
规则原文虽短,但"减少无谓遍历"的思想是普适的:它既是一条可被 Agent 精确检索执行的重构指令,也是人工代码评审时判断"这段数组操作是否可以更省"的快速标尺。在 Cherry Studio 这样以 Electron + React 大型渲染层为主要代码构成、且内置了完整 Agent 技能体系的仓库中,这类结构化规则显著降低了性能优化知识的传递成本。
【免费下载链接】cherry-studio🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考