- 人工智能
- 大模型
- 代码智能体
- AI Agent
- 桌面应用
- 后端
- 前端
- CLI
【免费下载链接】ZCode
ZCode 是 AI 编程工作台,提供桌面应用、浏览器界面和终端 Agent。本仓库包含客户端、后端服务、共享 UI,以及 Agent CLI 与运行时源码。
本篇指南讲解 React/Next.js 与 TypeScript 工程中一条高频性能规则——为重复查找构建索引 Map(Build Index Maps for Repeated Lookups)。该规则来自仓库内 vendored 的 Vercel React Best Practices 技能包(.agents/skills/react-best-practices/rules/js-index-maps.md),属于其中js-(JavaScript Performance)类别。读完本文,你将掌握:为什么循环内多次.find()会让算法退化为 O(n²),如何用一次 O(n) 建索引把后续每次查找降到 O(1),以及 ZCode 仓库源码中这一模式的实际落地形态(以 processTreeSnapshot.ts 与 taskIndexRepo.ts 为例),可直接用于日常代码评审与重构。
规则来源与本仓库上下文
js-index-maps.md是 ZCode 仓库.agents/skills/react-best-practices/目录下被 vendored 的性能准则之一。从 README.md 与 metadata.json 可以看到,该技能包源自 Vercel Engineering 的《React & Next.js 性能优化指南》,面向 AI Agent 与 LLM 的自动化重构而编写;SKILL.md 将全部规则按影响优先级分为 8 类,本规则位于第 7 类 JavaScript Performance(js-前缀,影响级别 LOW-MEDIUM),同级姊妹规则还包括js-set-map-lookups(Set/Map 做 O(1) 成员检查)、js-cache-property-access(循环内缓存属性访问)、js-cache-function-results(模块级 Map 缓存函数结果)等。
该规则的核心主张只有一句话:
多个
.find()调用按同一键查找时,应该改用 Map 索引。
这句话看起来简单,但在真实工程里是性能回归的高发点:只要在循环、.map()或嵌套遍历里按外层的某项(如userId)反复对另一份数组执行.find(),就会在无意识中制造 O(n²) 的算法复杂度。
反模式:循环内反复.find()(每次查找 O(n))
原文档给出的反例非常典型——用订单关联用户信息:
function processOrders(orders: Order[], users: User[]) { return orders.map((order) => ({ ...order, user: users.find((u) => u.id === order.userId), })); }假设orders有 M 个元素、users有 N 个元素,users.find()在无序数组上最坏情况下要线性扫描全部 N 个元素,外层orders.map()又执行 M 次,总复杂度为O(M × N)。当数据规模是 1000 × 1000 时,这意味着最多100 万次(1M)元素比较——而其中绝大多数比较都在重复访问同一批用户记录。
这也是.find()与indexOf/includes共有的陷阱:它们每一次调用都是对整个数组的线性扫描,扫描结果又不会缓存,因此"查一次、忘一次、再查一次"。
正解:一次建索引,之后全部 O(1)
原文档给出的正确写法是先把users按id预建为 Map,再用get()代替find():
function processOrders(orders: Order[], users: User[]) { const userById = new Map(users.map((u) => [u.id, u])); return orders.map((order) => ({ ...order, user: userById.get(order.userId), })); }这里的关键转变:
- 构建阶段:
new Map(users.map((u) => [u.id, u]))遍历一次users,把每个元素以其主键为键放入 Map,开销 O(N); - 查询阶段:
userById.get(order.userId)基于哈希表直接命中,每次查找 O(1),外层 M 次查找总计 O(M); - 整体复杂度:O(N + M),而不是 O(M × N)。
原文档给出的量化对比非常直观:
For 1000 orders × 1000 users:1M ops → 2K ops.
即同样处理 1000 个订单、关联 1000 个用户,从最多 100 万次比较下降到约 2000 次操作(建索引 1000 次 + 查询 1000 次),降幅达 500 倍,且数据规模越大收益越明显。
源码验证:ZCode 中索引 Map 的真实落地
该规则不仅是一条抽象建议,ZCode 仓库的源码里就有大量按"先建索引、再重复查询"模式组织的实现,可以作为可对照的实战范本。
进程树快照:按 parentPid 建索引后 DFS 多次查询
processTreeSnapshot.ts 的collectDescendantIdentitiesFromProcessList需要从系统进程表出发、递归收集某个根进程的所有后代。如果对每个节点都线性扫描整张进程表找其子进程,复杂度会退化得非常难看;源码的做法正是先构建索引:
function collectDescendantIdentitiesFromProcessList( rootPid: number, identities: readonly ProcessIdentity[], ): ProcessIdentity[] { const childrenByParentPid = new Map<number, ProcessIdentity[]>(); for (const identity of identities) { const children = childrenByParentPid.get(identity.parentPid) ?? []; children.push(identity); childrenByParentPid.set(identity.parentPid, children); } const descendants: ProcessIdentity[] = []; const seen = new Set<number>([rootPid]); const visit = (pid: number) => { for (const child of childrenByParentPid.get(pid) ?? []) { if (seen.has(child.pid)) { continue; } seen.add(child.pid); descendants.push(child); visit(child.pid); } }; visit(rootPid); return descendants; }可以看到:先一次遍历把所有进程按parentPid分组进Map<number, ProcessIdentity[]>(构建索引,O(n)),随后递归visit()里每次取子进程列表都是childrenByParentPid.get(pid)的 O(1) 哈希命中;同时seen用Set保证节点去重,避免环与重复访问。这与规则示例中"users.map(...)建 Map,orders.map(...)查 Map"是同构的两步走结构。
同一文件中的filterCurrentProcessIdentities(processTreeSnapshot.ts)也遵循同一模式:先用new Set(identities.map((i) => i.pid))建 PID 集合用于过滤进程表,再用currentByPid(一个Map)把"当前系统进程"按 PID 索引起来,最后只对需要复核的 identity 做 O(1) 的get比较。
任务索引仓库:Map/Set 用于去重与分组
session/taskIndexRepo.ts 中同样密集使用 Map/Set:
normalizeWorkspaceKeys(taskIndexRepo.ts)用new Set(...)对多个 scope 计算出的 workspace key 去重,再用sort排序,替代了低效的includes式去重;normalizeWorkspaceBootstrapScopes(taskIndexRepo.ts)用seen = new Set<string>()在循环中做 O(1) 重复检查,并在scope.workspacePurpose === "conversation"时提前跳过;bootstrapWorkspaceGroupsForActiveTasks中用candidateRowsByWorkspaceKey = new Map<string, TaskIndexRow[]>()把活跃任务按 workspace key 一次性分组,后续按组处理时全部走get。
这些代码说明:构建一次索引、重复 O(1) 查询在 ZCode 的后端服务(packages/services)里是稳定、被广泛采用的组织方式,评审代码时看到循环内array.find(() => ...)或array.includes(...)扫描另一份数组,都值得按本规则重构。
姊妹规则:Set 做 O(1) 成员关系检查
与本规则互补的是同级文件 js-set-map-lookups.md——当需求只是"判断某 ID 是否在允许列表中"(成员关系检查)时,用Set而非Map:
// 反例:每次 includes 都线性扫描 const allowedIds = ['a', 'b', 'c', ...] items.filter(item => allowedIds.includes(item.id)) // 正例:Set.has() 为 O(1) const allowedIds = new Set(['a', 'b', 'c', ...]) items.filter(item => allowedIds.has(item.id))规则选择口诀:需要"按键取对象"用 Map(get),只需要"是否包含"用 Set(has)。两者的共同原理都是哈希表 O(1) 命中,代价都是一次 O(n) 构建。
适用场景与注意事项
结合规则原文与源码实践,落地时请注意以下几点:
- 只在"重复查找"时建索引:如果某数组只在循环外被查找一次,
find()本身没问题;本规则针对的是循环体 /.map()回调 / 递归中反复执行同键查找的场景。 - 键的选择要稳定唯一:示例用
u.id作为键,实际项目建议使用主键、枚举常量或可规范化字符串;若键存在大小写、空格差异,先归一化再入 Map(参考 taskIndexRepo.ts 中normalizeSearchSnippetText的归一化思路)。 - Map 与普通对象(Record)的取舍:Map 支持任意键类型(含对象、
number),遍历有序,且不会受原型链污染;键为字符串且数量稳定时用Record<string, T>也常见。在 processTreeSnapshot.ts 中键是number(PID),因此选用Map<number, ...>更合适。 - 内存换时间的权衡:索引会为 N 个元素额外保留一份引用(哈希表结构),在 N 极大时需评估内存开销;多数业务场景(千级、万级)收益远大于成本。
- 与不可变数据、React 生态的配合:若底层数组来自 props/state 且可能变化,索引应在每次数据变化后重建或增量更新;在 React 组件/渲染函数内,建议把建索引与查询放在同一作用域,避免把可变索引提升到模块级造成脏数据(仓库中
server-no-shared-module-state等规则同样反对模块级共享可变状态)。
相关文件索引
- 规则原文:.agents/skills/react-best-practices/rules/js-index-maps.md
- 姊妹规则(Set/Map O(1) 查找):.agents/skills/react-best-practices/rules/js-set-map-lookups.md
- 规则分类与优先级:.agents/skills/react-best-practices/SKILL.md
- 技能包说明:.agents/skills/react-best-practices/README.md
- 源码佐证一(进程树按 parentPid 建索引):processTreeSnapshot.ts
- 源码佐证二(任务索引 Map/Set 应用):taskIndexRepo.ts
总结成一句话:当循环体里出现第二次线性扫描时,先花 O(n) 建一张 Map/Set 索引,把"每次查一遍"换成"查哈希表一次"——这就是js-index-maps这条规则的全部精髓,也是从 1M 次操作降到 2K 次操作的全部秘密。
- 人工智能
- 大模型
- 代码智能体
- AI Agent
- 桌面应用
- 后端
- 前端
- CLI
【免费下载链接】ZCode
ZCode 是 AI 编程工作台,提供桌面应用、浏览器界面和终端 Agent。本仓库包含客户端、后端服务、共享 UI,以及 Agent CLI 与运行时源码。
相关推荐
Polar 前端优化实践:用 Map 索引替代重复 find 查找,将 O(n²) 降为 O(n)
Polar 前端优化实践:用 Map 索引替代重复 find 查找,将 O n² 降为 O n 本篇技术指南来自 Polar 仓库内置的 Vercel Reac
后端前端金融科技Metahuman-Stream 数字人直播部署:环境、推流与参数配置全清单
Metahuman Stream 数字人直播部署:环境、推流与参数配置全清单 Metahuman Stream 是一个实时交互的数字人直播引擎:输入文字或语音,
人工智能大模型数字人语音音视频媒体生成后端OpenMetadata 前端性能优化:用 Set/Map 将 O(n) 成员查找降为 O(1)
OpenMetadata 前端性能优化:用 Set/Map 将 O n 成员查找降为 O 1 本文基于 OpenMetadata 仓库内 vendored 的
数据目录数据血缘数据治理后端MCP 服务
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考