【免费下载链接】open-slide
A slide framework built for agents.
本文是 open-slide 项目中 Vercel React Best Practices 技能集 中 JavaScript 性能优化规则js-set-map-lookups的深度展开。它解决的是前端/编辑器类应用中一个非常典型的问题:在数组上反复执行includes、find等线性查找,随着数据规模增长,主线程计算量呈 O(n²) 级膨胀,拖慢渲染与交互。读完本文,你将掌握 Set/Map 的数据结构选型依据、正确的替换写法,以及如何结合 open-slide 源码 中的真实案例(如画布元素选取、文件夹索引、PPTX 导出图片映射)落地这套优化。
一、规则概览:从 O(n) 到 O(1)
原规则文件位于 .agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md,属于 8 大分类中的JavaScript Performance(影响等级 LOW-MEDIUM,标注impactDescription: O(n) to O(1))。其核心主张只有一句话:
将数组转换为 Set/Map,用于重复的成员资格检查(membership checks)。
数组的includes、indexOf、find都是线性扫描——每检查一个元素就要从头遍历数组,单次开销 O(n)。而Set.prototype.has与Map.prototype.get基于哈希表实现,单次开销 O(1)(平均情况下)。当同样的检查在一个循环、一次渲染、或一个热路径函数里反复执行时,二者的差距会被放大为 O(n × m) 与 O(n + m),这就是本规则存在的意义。
二、规则的原始示例:错误的 O(n) 写法
原规则给出的典型反例是白名单过滤:
const allowedIds = ['a', 'b', 'c', ...] items.filter(item => allowedIds.includes(item.id))问题在于:filter对items的每一项都会调用allowedIds.includes(item.id),而includes每次都是对整个allowedIds数组做一次线性遍历。设items有 N 项、allowedIds有 M 项,总比较次数为N × M。当白名单与数据量同时增长到数百、上千级别,主线程就会产生肉眼可见的卡顿——在 open-slide 这类需要实时响应拖拽、缩放、编辑操作的编辑器场景中尤其不可接受。
三、正确的 O(1) 写法
原规则的推荐写法是把数组一次性构造成Set,把“扫描”变成“哈希命中”:
const allowedIds = new Set(['a', 'b', 'c', ...]) items.filter(item => allowedIds.has(item.id))关键差异:
- 构建 Set 的成本:
new Set(array)只需一次 O(M) 的遍历,且通常在循环之外执行; - 每次检查的成本:
has是 O(1),于是整个过滤过程从 O(N × M) 降为O(N + M); - 可读性:
allowedIds.has(item.id)语义上比allowedIds.includes(item.id)更明确地表达“这是集合成员判断”,代码意图更清晰。
补充一个 TypeScript 细节:new Set(['a', 'b', 'c', ...])会按字面量自动推断为Set<string>;当元素来自更宽泛的类型(如string | undefined)时,可显式标注new Set<string>(...)或在构造时用filter(Boolean)收窄,避免has的参数类型告警。
四、Set 与 Map 如何选择
原规则标题同时提到了 Set 和 Map,二者对应两种不同的使用场景:
| 场景 | 数据结构 | 关键方法 | 适用问题 |
|---|---|---|---|
| 只关心“是否存在”,不需要关联数据 | Set<T> | has/add/delete | 白名单过滤、去重、标签判定 |
| 需要“按 key 取出关联数据” | Map<K, V> | get/set/has | 建立 id → 对象的索引表 |
选择原则很简单:只需要成员判断就用 Set,需要取回值就用 Map。二者在 open-slide 源码中都有大量印证,见下一节。
五、open-slide 源码中的真实实践
这套规则并非纸上谈兵,open-slide 的packages/core中可以看到多处一致的实现模式。
1. 资源重名校验:用 Set 做成员判断
在 packages/core/src/app/lib/assets.ts 中,生成新资源名时会构造当前已占用名称的集合:
const taken = new Set(list.map((a) => a.name));后续每一次“名称是否可用”的检查都走taken.has(name),避免了对资源列表反复线性扫描——这与原规则的白名单示例是同一个模式。
2. 文件夹索引:用 Map 做 id → 对象映射
在 packages/core/src/app/lib/folders.ts 中,通过map+ 二元组数组直接构造 Map:
const byId = new Map(prev.folders.map((f) => [f.id, f]));这正是原规则希望推广的“先建索引、再 O(1) 查询”范式:new Map(entries.map(e => [e.key, e]))一步到位。
3. PPTX 导出:图片 id 映射
在 packages/core/src/app/lib/pptx/ooxml.ts 中,导出 OOXML 时同样先建立图片索引:
const imageById = new Map(deck.images.map((img) => [img.id, img]));PPTX 导出需要按 id 反复引用图片关系,用 Map 让每次引用都是 O(1) 命中,而不是每次images.find(img => img.id === id)。
4. 静态白名单集合:标签判定
open-slide 还用 Set 固化“静态枚举判定”,例如 packages/core/src/app/lib/pptx/measure.ts 中的媒体标签集合、packages/core/src/app/components/inspector/inline-text-editor.tsx 中的行内样式键集合:
const RANGE_STYLE_KEYS = new Set(['fontSize', 'fontWeight', 'fontStyle', 'fontFamily', 'color']);对于这类“固定且频繁判定的枚举”,模块级Set比数组includes更快,也比switch分支更易扩展维护。
5. 可视化编辑器的选区集合
在 packages/core/src/app/lib/inspector/use-visual-editor.ts 中,编辑选区相关逻辑使用new Set(targets.map(target => target.anchor))收集目标元素——选中多个元素后要反复判定“某元素是否被选中”,Set 的has使每次判定都是常数时间。
从上述案例可以推断出本规则在 open-slide 中的典型适用面:编辑器状态判定、资源/文件夹索引、导出管线中的对象映射、以及渲染循环内的白名单过滤。
六、进阶变体一:构建索引 Map 避免重复 find
与js-set-map-lookups同属 JavaScript Performance 分类的 js-index-maps 规则 给出了它的“连招”:当需要按同一 key 反复从数组中查找对象时,不要写多个.find(),而是先建一次 Map。
错误写法(每个订单都要线性查找一次用户):
function processOrders(orders: Order[], users: User[]) { return orders.map(order => ({ ...order, user: users.find(u => u.id === order.userId) })) }正确写法(先建索引,查询全部 O(1)):
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) })) }该规则给出了一个直观的量化对比:1000 个订单 × 1000 个用户,朴素写法是 100 万次比较(1M ops),建索引后降到约 2000 次操作(2K ops)。open-slide 中 folders.ts 的byId与 ooxml.ts 的imageById正是这一变体的落地。
七、进阶变体二:模块级 Map 缓存重复函数调用
再进一步,js-cache-function-results 规则 展示了 Map 的另一种用法——模块级缓存:当渲染期间同一函数被以相同输入反复调用时,用模块级 Map 记住结果:
// Module-level cache const slugifyCache = new Map<string, string>() function cachedSlugify(text: string): string { if (slugifyCache.has(text)) { return slugifyCache.get(text)! } const result = slugify(text) slugifyCache.set(text, result) return result }该规则特别强调:用 Map 而非 React Hook,是因为它可以在工具函数、事件处理器等一切位置使用,不局限于组件内部。这对 open-slide 这类“组件树之外还有大量 lib 逻辑”的代码库非常关键。
八、注意事项与适用边界
任何优化都有前提,使用 Set/Map 时应留意以下几点:
- 只在“重复检查”时替换:如果某数组只被查询一次,
includes与has的差距可以忽略,引入 Set 反而多一次 O(n) 的构建成本。规则的适用前提是“repeated membership checks”。 - 对象成员用引用相等:
Set.has与Map.get对对象使用引用比较(SameValueZero),若数组元素是每次新建的对象字面量,集合可能永远“命中不了”。需要按字段比较时应先归一化 key(如map(o => o.id))。 - 大数据集的构建时机:Set/Map 的构建应放在循环与热路径之外(如模块级或
useMemo)。例如 open-slide 在 asset-view.tsx 中使用useMemo(() => new Set(assets.map(asset => asset.name)), [assets]),让依赖变化时才重建集合。 - 内存与顺序:Set/Map 保持插入顺序,但不提供按值排序的语义;若后续逻辑依赖原数组顺序,可保留原数组,仅在判断路径使用集合。
九、小结
js-set-map-lookups是一条小而实用的规则:把数组includes换成Set.has、把find换成Map.get,用一次 O(n) 的建表成本,换取后续所有查询的 O(1)。它在 open-slide 中的价值不止于白名单过滤——从资源重名校验(assets.ts)、文件夹索引(folders.ts)、PPTX 图片映射(ooxml.ts)到可视化编辑器的选区判定(use-visual-editor.ts),全部遵循同一模式。配合 js-index-maps 与 js-cache-function-results 两条相邻规则,你可以在渲染循环、导出管线与工具函数三个层面系统性地消除线性查找瓶颈。
【免费下载链接】open-slide
A slide framework built for agents.
相关推荐
Phoenix 前端性能优化指南:用 Set/Map 将 JavaScript 重复查找从 O(n) 降到 O(1)
Phoenix 前端性能优化指南:用 Set/Map 将 JavaScript 重复查找从 O n 降到 O 1 本指南源自当前仓库 .agents/skill
可观测性AI 评测LLMOpsAI 应用人工智能Langfuse 前端性能优化:用 Set/Map 将重复查找从 O(n) 降到 O(1)
Langfuse 前端性能优化:用 Set/Map 将重复查找从 O n 降到 O 1 本文基于 Langfuse 仓库内 web/.agents/skills
人工智能LLMOps可观测性AI 评测LLM 网关后端前端Cherry Studio 性能优化实战:用 Set/Map 将 JavaScript 成员查找从 O(n) 降到 O(1)
Cherry Studio 性能优化实战:用 Set/Map 将 JavaScript 成员查找从 O n 降到 O 1 本篇技术指南基于 Cherry Stu
AI 应用大模型桌面应用本地部署RAG
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考