Cal.diy 性能规则解析:合并多次数组遍历,将 N 次迭代压缩为 1 次
本文基于 Cal.diy(Cal.com 开源版)仓库内置的 Vercel React 最佳实践技能库,深入讲解 js-combine-iterations 规则:为什么对同一数组发起多次 .filter() / .map() 是应被识别和重构的性能反模式,如何将其合并为单次循环,以及在当前代码库中这种模式出现在哪些真实场景里。读完本文,你将掌握该规则的完整判定标准、改写范式,并能结合仓库源码验证其实际应用与边界。
一、规则定位:45 条性能规则中的 JavaScript 分类
该规则定义在 js-combine-iterations.md,其 YAML 元数据声明了规则的完整画像:
| 元数据字段 | 取值 | 含义 |
|---|---|---|
title |
Combine Multiple Array Iterations | 合并多次数组遍历 |
impact |
LOW-MEDIUM | 影响等级为中低(属渐进式优化,非关键路径瓶颈) |
impactDescription |
reduces iterations | 核心收益:减少迭代次数 |
tags |
javascript, arrays, loops, performance | 标签覆盖 JS、数组、循环、性能四个维度 |
这个技能库的总览文件 SKILL.md 将 45 条规则分为 8 个按影响优先级排列的类别。js-combine-iterations 位于第 7 类「JavaScript Performance(LOW-MEDIUM)」,与 js-index-maps(为重复查找构建 Map)、js-set-map-lookups(Set/Map 的 O(1) 查找)、js-early-exit(函数提前返回)、js-min-max-loop(用循环代替排序求极值)等规则并列。这意味着它和消除瀑布式等待(async- 类,CRITICAL)、包体积优化(bundle- 类,CRITICAL)不在同一量级,而是在热点循环内部做常数级迭代次数削减的精细优化。
规则的完整展开版本见技能库的编译文档 AGENTS.md(第 7.6 节),内容与独立规则文件完全一致,可作为 LLM/Agent 执行自动重构时的引用入口。
二、规则核心:反模式识别与改写范式
规则原文给出的判定标准是一句话:
Multiple
.filter()or.map()calls iterate the array multiple times. Combine into one loop. (多次.filter()或.map()调用会使数组被遍历多次。将它们合并进一个循环。)
2.1 反模式:3 次遍历
const admins = users.filter(u => u.isAdmin)
const testers = users.filter(u => u.isTester)
const inactive = users.filter(u => !u.isActive)
三段代码看似各不相干,但每次调用都完整扫描一遍 users:3 次数组遍历、3 次迭代器创建、3 个中间数组的分配。若 users 长度为 n,总工作量是 O(3n)。
2.2 正确写法:1 次遍历
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)
}
改写要点有三:
- 先声明结果数组(带显式类型标注
User[]),在循环外一次性分配; - 单循环内并行判断:每个元素只做一次属性读取和一次循环推进,多个条件互不短路、互不影响,因此
admins、testers、inactive三个集合与多次 filter 的结果逐元素等价; push摊还 O(1),总复杂度从 O(kn)(k 为遍历次数)降为 O(n),且只创建 1 个迭代器。
这里有一个容易被忽视的等价性细节:原例中 isAdmin、isTester、!isActive 是相互独立的谓词(一个用户可能既是 admin 又是 tester),所以合并为多个 if 是正确的;如果各谓词互斥(命中其一即应停止),则应改用 if / else if 链,语义与性能会不同。改写时必须先确认谓词之间是否互斥,这是应用该规则时的首要检查点。
三、仓库实证:Cal.diy 代码库中的真实案例
该规则并非纸面理论,当前仓库源码中存在符合该模式的真实代码。RegularBookingService.ts 中处理团队事件的成员筛选逻辑:
const teamDestinationCalendars: DestinationCalendar[] = [];
const fixedUsers = users.filter((user) => user.isFixed);
const nonFixedUsers = users.filter((user) => !user.isFixed);
const filteredUsers =
schedulingType === SchedulingType.ROUND_ROBIN ? [...fixedUsers, ...nonFixedUsers] : users;
这里 users 被连续两次 .filter() 扫描,且两个谓词(user.isFixed / !user.isFixed)互斥——这正是 2.2 节强调需要区分的场景。按该规则的范式,这段代码可以改写为单循环分桶:
const fixedUsers: typeof users = [];
const nonFixedUsers: typeof users = [];
for (const user of users) {
if (user.isFixed) {
fixedUsers.push(user)
} else {
nonFixedUsers.push(user)
}
}
值得注意的是,此处保留双 filter 写法有一定现实合理性:团队事件的用户列表规模有限(一个团队成员通常几十人量级),而 ROUND_ROBIN 模式下「固定用户排前、非固定用户排后」的拼接意图([...fixedUsers, ...nonFixedUsers])在双 filter 版本中更直白。这也体现了该规则 impact: LOW-MEDIUM 的定位——它是应被知晓的优化项,但不是无条件强制项,在数据规模小、可读性收益明显时保留原写法是合理权衡。
此外,data-table GUIDE.md 的示例代码中也出现了 users 上 activeUsers 与 inactiveUsers 两次 filter 的同类结构,进一步说明「同一数组、互补谓词、双 filter 分桶」是代码库中反复出现的惯用结构,值得作为该规则的标准识别模式。
四、纵深关联:与 O(n²) 规避规则的关系
Cal.diy 的 Agent 规则库中另有一条 CRITICAL 级规则 performance-avoid-quadratic.md,明确把「链式 filter(Chained filters)和嵌套映射」列为 O(n²) 反模式的来源之一:
- Chained filters or nested mapping over large lists
- Array methods like
.some,.find, or.filterinside loops or callbacks
两条规则构成互补关系:js-combine-iterations 处理的是同一数组上的横向冗余遍历(k 次完整扫描 → 1 次),属于常数因子优化;而 performance-avoid-quadratic 处理的是谓词内部又对另一数组做线性扫描(.filter 回调里嵌套 .some),属于复杂度量级优化。前者将 O(kn) 压到 O(n),后者将 O(n·m) 压到 O((n+m) log(n+m))。在代码审查中同时应用两者时,优先级应以后者为先——量级下降永远优于常数优化,这与技能库把 js- 类规则排在 CRITICAL/HIGH 规则之后的分类逻辑一致。
五、适用边界与判定清单
结合规则原文与仓库实践,将该规则的适用条件整理为一份可操作清单:
- 适用:同一数组在相同数据状态下被 2 次及以上遍历;各谓词相互独立且只做读取(无副作用、无中间状态依赖);数据规模足以让多次扫描产生可测量开销(热路径、大数组、服务端批量处理)。
- 需谨慎:谓词互斥时改用
if / else if(见 RegularBookingService 案例);谓词之间存在依赖(第二个 filter 的输入是第一个 filter 的输出)时不能合并,那是流水线而非冗余遍历;数组规模极小且原代码意图自明时,可读性可能优先于常数优化。 - 不适用:遍历次数本身很少(单次 filter/map);不同数组之间的多次遍历(合并无从谈起);异步谓词场景(单循环内
await会串行化,应优先考虑Promise.all等并发手段,对应技能库async-parallel规则)。
六、小结
js-combine-iterations 是 Vercel React 最佳实践技能库中 JavaScript 性能分类下的基础规则,核心主张简单而明确:对同一数组的多次 .filter() / .map() 应合并为单次循环,用循环内分支判断替代多次完整扫描。在 Cal.diy 这样的企业级调度基础设施代码库中,团队事件成员筛选等场景存在真实的多次遍历结构;将其与 CRITICAL 级的 O(n²) 规避规则区分开——一个管常数、一个管量级——可以在代码审查与 AI 辅助重构中做出正确的优先级判断。规则文件、完整技能目录与上文引用的源码路径均保留在当前仓库中,可供进一步查证。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00