- 人工智能
- 大模型
- 代码智能体
- AI Agent
- 桌面应用
- 后端
- 前端
- CLI
【免费下载链接】ZCode
ZCode 是 AI 编程工作台,提供桌面应用、浏览器界面和终端 Agent。本仓库包含客户端、后端服务、共享 UI,以及 Agent CLI 与运行时源码。
ZCode(智谱 AI 编程工作台)前端采用 React/TypeScript 技术栈,其仓库内置了源自 Vercel 工程团队的 React/Next.js 性能优化规则集(.agents/skills/react-best-practices),用于指导自动化代码生成与重构。本文聚焦其中 JavaScript 性能类别下的js-cache-function-results规则:当同一函数在渲染期间以相同入参被反复调用时,用模块级 Map 缓存计算结果(memoization)。读完本文,你将掌握模块级缓存的标准写法、单值函数的简化模式、缓存失效策略,以及它在 ZCode 仓库(如代码高亮模块)中的真实落地形态。
规则背景:这条规则从哪来、优先级如何
js-cache-function-results是 ZCode 仓库中 vendored 的 react-best-practices 技能 下的 70 条规则之一。该技能将规则按影响优先级分为 8 类,本文主题属于第 7 类JavaScript Performance(LOW-MEDIUM 优先级),前缀为js-:
| 优先级 | 类别 | Impact | 前缀 |
|---|---|---|---|
| 7 | JavaScript Performance | LOW-MEDIUM | js- |
单条规则文件的 frontmatter 声明了它的元信息:
- title: Cache Repeated Function Calls
- impact: MEDIUM
- impactDescription: avoid redundant computation(避免冗余计算)
- tags: javascript, cache, memoization, performance
在完整编译版 AGENTS.md 中,它位于7.4 Cache Repeated Function Calls小节。规则的适用场景是"writing, reviewing, or refactoring React/Next.js code"——即编写新组件、审查代码或做性能重构时,应主动检查是否存在相同入参的重复函数调用。
问题场景:渲染期对相同输入反复调用纯函数
规则给出的反面示例是一段典型的 React 列表渲染代码:ProjectList组件对每个 project 调用一次slugify()来生成 slug,但项目列表可能包含大量重名项目,导致同一名称被重复计算上百次:
function ProjectList({ projects }: { projects: Project[] }) { return ( <div> {projects.map(project => { // slugify() called 100+ times for same project names const slug = slugify(project.name) return <ProjectCard key={project.id} slug={slug} /> })} </div> ) }这里的核心问题是:slugify是纯函数(相同输入必然产生相同输出),但每次渲染都重新执行一遍。当列表规模大、函数计算成本高(例如字符串处理、正则替换、编码转换)时,这部分开销会随渲染次数线性放大——每次 state 变化触发重渲染,整个列表都要重新计算一遍。
判断是否值得缓存的三个条件:
- 函数是确定性的(相同输入 → 相同输出),如
slugify、正则替换、JSON.stringify格式化; - 同一输入在多次渲染周期内反复出现;
- 单次计算成本可观(不是简单的算术或属性读取)。
满足以上条件时,缓存收益显著;否则引入缓存反而是过早优化。
解决方案:模块级 Map 缓存
规则给出的正确写法是把缓存提升到模块作用域,用一个Map<string, string>记录已计算过的输入-输出对:
// 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 } function ProjectList({ projects }: { projects: Project[] }) { return ( <div> {projects.map(project => { // Computed only once per unique project name const slug = cachedSlugify(project.name) return <ProjectCard key={project.id} slug={slug} /> })} </div> ) }要点拆解:
- 缓存生命周期:Map 声明在模块顶层,跨组件实例、跨渲染周期存活,组件卸载后缓存仍在,后续重新挂载可直接命中;
- 命中路径:
has()+get()是 O(1) 查找,命中后直接返回,不再调用slugify; - 写入路径:未命中才执行真实计算,并
set()记录结果; - Map 键类型:规则示例是
Map<string, string>,理论上可用任意可区分类型的键;若键是对象,需注意引用相等语义(见下文"适用边界")。
通用化:封装成可复用的缓存函数
在大型代码库中,同一模式会反复出现。可以将"缓存包裹"抽象为一个小工具函数,例如:
function memoize<TArgs extends unknown[], TResult>(fn: (...args: TArgs) => TResult): (...args: TArgs) => TResult { const cache = new Map<string, TResult>() return (...args: TArgs) => { const key = JSON.stringify(args) if (cache.has(key)) { return cache.get(key)! } const result = fn(...args) cache.set(key, result) return result } } const cachedSlugify = memoize(slugify)注意:JSON.stringify作键要求参数可序列化且序列化结果唯一,复杂对象入参时需要谨慎。
单值函数的简化模式:单个变量即可
当函数只有一个"当前值"(而不是一组输入-输出映射)时,规则建议用单个模块级变量代替 Map,更轻量:
let isLoggedInCache: boolean | null = null; function isLoggedIn(): boolean { if (isLoggedInCache !== null) { return isLoggedInCache; } isLoggedInCache = document.cookie.includes("auth="); return isLoggedInCache; } // Clear cache when auth changes function onAuthChange() { isLoggedInCache = null; }这个模式适合"全局唯一状态"类函数:缓存值只有一个,用null作为"未缓存"哨兵值,第一次调用时计算并写入,之后直接返回。关键配套动作是提供失效入口——onAuthChange()将缓存重置为null,保证登录态变化后重新读取。
同样的思路也适用于其他同步昂贵的单值来源。在 ZCode 仓库中,js-cache-storage.md 规则就给出了 cookie 缓存的变体:用Record<string, string> | null缓存document.cookie解析结果,避免每次调用都同步解析 cookie 字符串。
为什么用 Map 而不是 Hook
规则明确强调:Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
这是本规则与 React 生态常见做法的关键区别:
- React Hook(如
useMemo)只能在组件/自定义 Hook 中调用,且缓存随组件实例销毁而失效,同一组件卸载重挂载后缓存即丢失; - 模块级 Map 属于纯 JS 机制,可以在工具函数、事件处理器、非组件模块中直接使用,适用范围更广;
- 模块级缓存在组件实例间共享,多个列表、多次渲染复用同一份结果。
因此,这条规则适用于一切"非组件作用域"的重复计算场景,而不只是 React 渲染路径。它属于 JavaScript 性能优化而非 React 专属优化。
缓存失效:外部可变数据必须显式清理
规则正文要求 "Clear cache when auth changes",这是模块级缓存最重要的工程纪律:凡是底层数据可能被外部改变的场景,都必须提供失效机制,否则会读到陈旧值。
结合 js-cache-storage.md 中的配套实践,典型的失效触发源包括:
// 存储可能在外部变化(另一个标签页、服务端设置的 cookie),需要失效缓存: window.addEventListener("storage", (e) => { if (e.key) storageCache.delete(e.key); }); document.addEventListener("visibilitychange", () => { if (document.visibilityState === "visible") { storageCache.clear(); } });失效策略取舍(可结合 LRU 缓存思路,参见同目录 server-cache-lru.md):
| 失效触发点 | 实现方式 | 适用场景 |
|---|---|---|
| 业务事件 | 显式暴露clearCache()/ 置null | 登录态变化、配置更新 |
| 跨标签页 | storage事件删除对应 key | localStorage/sessionStorage |
| 页面重新可见 | visibilitychange后clear() | cookie、服务端下发的数据 |
| 容量上限 | 限制 Map 大小 / LRU 淘汰 | 无限增长的高基数输入(如代码块) |
仓库实证:ZCode 中代码高亮缓存的真实实现
规则并非纸上谈兵,ZCode 仓库的 UI 层就有一个高度契合的落地案例:packages/ui/src/lib/shikiHighlighter.ts 的 Shiki 代码高亮模块。它用多个模块级 Map缓存不同粒度的计算结果:
const highlighterCache = new Map< string, Promise<HighlighterGeneric<BundledLanguage, BundledTheme>> >(); const tokensCache = new Map<string, TokenizedCode>(); const subscribers = new Map<string, Set<(result: TokenizedCode) => void>>(); // 内存诊断计数器:tokensCache 目前无淘汰,是审计里 // renderer 最可疑的增长点,先把条数落到日志里。 uiMemoryDiagnosticsRegistry.register("shiki", () => ({ tokensCache: tokensCache.size, highlighters: highlighterCache.size, }));从中可以观察到规则的完整实践形态:
- highlighterCache:缓存
createHighlighter()的 Promise,按theme:language组合建键——避免同一主题/语言重复创建昂贵的 Shiki 高亮器实例; - tokensCache:缓存
TokenizedCode词法分析结果,键由getCodeTokensCacheKey()构造(theme:language:length:head:tail),在 highlightCode 入口 命中后直接返回缓存结果,并推迟到微任务通知订阅者,避免同步 setState 触发 React 嵌套更新; - subscribers:缓存每个 cacheKey 的回调订阅集合,供异步高亮完成后的分发使用。
更有价值的是,该文件还展示了模块级缓存必须配套的容量治理:注释明确承认tokensCache目前无淘汰、是 renderer 最可疑的内存增长点,并借助uiMemoryDiagnosticsRegistry把两个缓存的大小实时登记到内存诊断日志中。这印证了规则的深层含义——缓存本质是用内存换时间,基数无界时必须考虑淘汰与观测。而缓存命中时通过queueMicrotask异步回调的设计,则体现了"缓存不只是减少计算,还要与渲染调度协同"的工程细节。
与相邻规则协同:形成一套完整的缓存优化体系
js-cache-function-results并非孤立规则,它与同类别规则构成互补,在实际重构中常组合使用:
| 规则文件 | 主题 | 与本规则的分工 |
|---|---|---|
| js-index-maps.md | 为重复查找构建索引 Map | 把 O(n) 的find循环变成 O(1) 的 Map 查找(1M ops → 2K ops) |
| js-cache-property-access.md | 循环内缓存属性访问 | 把热路径中重复的属性链取值提升到循环外 |
| js-cache-storage.md | 缓存 Storage/Cookie 读取 | 把同步昂贵的 I/O 读取缓存到内存并处理失效 |
| server-cache-react.md | 服务端React.cache() | 服务端按请求去重,与本规则的客户端模块级缓存对应 |
组合原则:先识别热点(哪个函数被重复调用)、再选缓存容器(Map / 单值变量)、最后定失效策略。这与 js-set-map-lookups.md(用 Set/Map 做 O(1) 查找)一脉相承,共同构成了"减少重复工作"的优化主线。
适用边界与注意事项
- 只缓存纯函数:依赖外部可变状态(时间、随机数、用户输入、DOM 实时值)的函数缓存会返回陈旧结果;
- 警惕内存增长:高基数字符串输入(如代码高亮的每段代码)会让 Map 无限膨胀,需要容量上限或 LRU 淘汰(参见 server-cache-lru.md 的思路迁移到客户端);
- 多实例语义:模块级缓存是全局共享的,如果不同组件希望拿到不同版本的结果,需把区分维度纳入缓存键;
- SSR/同构环境:模块级可变状态在服务端渲染(RSC/SSR)场景可能跨请求泄漏,应遵循同目录 server-no-shared-module-state.md 的约束,仅在客户端模块或明确安全的场景使用;
- 可观测性:参考 shikiHighlighter 的做法,为缓存接入诊断/日志,出现内存问题时能快速定位。
小结
js-cache-function-results规则给出的方案简洁而通用:纯函数 + 模块级 Map + 显式失效。它用最少的代码消除了渲染期最高频的冗余计算,且不依赖 React 运行时,可在组件、工具函数、事件处理器中统一生效。ZCode 仓库中 Shiki 高亮模块的多级 Map 缓存与内存诊断实践,展示了这一模式在真实产品中的完整落地形态——既要缓存命中率,也要关注缓存本身的规模与生命周期。在编写或重构 React 组件时,若发现"同一输入被反复计算",优先考虑这条规则,往往能以最小改动获得稳定可观的性能收益。
- 人工智能
- 大模型
- 代码智能体
- AI Agent
- 桌面应用
- 后端
- 前端
- CLI
【免费下载链接】ZCode
ZCode 是 AI 编程工作台,提供桌面应用、浏览器界面和终端 Agent。本仓库包含客户端、后端服务、共享 UI,以及 Agent CLI 与运行时源码。
相关推荐
React 重复函数调用缓存:用模块级 Map 消除冗余计算(Vercel React 性能实践)
React 重复函数调用缓存:用模块级 Map 消除冗余计算(Vercel React 性能实践) 导读 本文围绕 Vercel Engineering 发布的
音视频桌面应用后端OpenMontage 前端性能优化:用模块级 Map 缓存重复函数调用,消除渲染期冗余计算
OpenMontage 前端性能优化:用模块级 Map 缓存重复函数调用,消除渲染期冗余计算 导读 在 React 组件渲染过程中,对同一组输入反复调用相同的纯
人工智能AI Agent音视频媒体生成工作流自动化缓存重复函数调用:在 cal.com 前端用模块级 Map 消除渲染中的冗余计算
缓存重复函数调用:在 cal.com 前端用模块级 Map 消除渲染中的冗余计算 导读:本文基于 cal.diy(cal.com 开源调度平台)仓库内置的 ve
后端前端企业应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考