
Langfuse 前端性能优化实践用模块级 Map 缓存函数结果消除渲染期的重复计算【免费下载链接】langfuse Open source AI engineering platform: LLM evals, observability, metrics, prompt management, playground, datasets. Integrates with OpenTelemetry, LangChain, OpenAI SDK, LiteLLM, and more. YC W23项目地址: https://gitcode.com/GitHub_Trending/la/langfuse模块级缓存module-level memoization是 Vercel Engineering 沉淀的 React/Next.js 性能优化最佳实践之一当同一函数在渲染过程中被以相同入参反复调用时用模块作用域下的Map缓存计算结果把 O(n) 次重复计算降为 O(1) 次查表。本文以 langfuse 开源仓库为背景完整讲解这条规则规则文件位于 web/.agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md的适用场景、两种标准写法、失效处理与使用边界并结合 langfuse 前端源码trace 图构建、Markdown 渲染、导航过滤等说明该模式在生产代码中的真实形态。读完后你可以在自己的组件、工具函数与事件处理器中直接套用这套模式。规则出处它是 Vercel 最佳实践体系中的哪一环在 langfuse 仓库中web/.agents/skills/vercel-react-best-practices/目录存放了一套完整的 React/Next.js 性能优化指南由技能总纲 SKILL.md 与 57 个独立规则文件组成共分为 8 大优先级类别优先级类别影响级别前缀1Eliminating Waterfalls消除瀑布式请求CRITICALasync-2Bundle Size Optimization包体积优化CRITICALbundle-3Server-Side Performance服务端性能HIGHserver-4Client-Side Data Fetching客户端数据获取MEDIUM-HIGHclient-5Re-render Optimization重渲染优化MEDIUMrerender-6Rendering Performance渲染性能MEDIUMrendering-7JavaScript PerformanceJS 运行时性能LOW-MEDIUMjs-8Advanced Patterns高级模式LOWadvanced-本文讲解的js-cache-function-results属于第 7 类 JavaScript Performance。该规则文件自述的元信息为影响级别 MEDIUM、影响描述avoid redundant computation避免冗余计算、标签为javascript, cache, memoization, performance。它和同目录下的js-index-maps用 Map 做 O(1) 查找、js-cache-storage缓存 localStorage/sessionStorage 读取、js-cache-property-access循环内缓存对象属性访问、server-cache-lru服务端 LRU 缓存、server-cache-react用React.cache()做每请求去重共同构成了一套从客户端到服务端的缓存方法论。本文这条规则解决的是客户端渲染路径中、模块级函数层的重复计算问题。问题场景为什么渲染期重复调用是性能隐患React 组件的 render 是纯函数同一组件可能因父组件状态变化、路由切换、数据刷新而被反复执行。如果 render 内部直接对一批数据进行逐个的昂贵计算字符串规范化、slug 化、正则解析、格式化、排序等那么同一份输入在单次渲染内被重复计算比如projects.map()遍历 100 个项目若其中很多项目名称相同slugify(project.name)会被重复执行 100 次而输入完全相同的结果却只有几个每次渲染都从头重算只要组件 re-render无论输入是否变化计算全部重来一遍。规则文件给出的错误示范非常直观摘录自 js-cache-function-results.mdfunction 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()会对相同的项目名被调用 100 次。这种同输入、同输出、重复计算的函数即纯函数正是缓存的理想对象。核心模式一模块级Map缓存多值函数规则给出的正确示范是在模块作用域声明一个Mapstring, string用先查缓存、未命中则计算并写入的经典模式包装原函数// Module-level cache const slugifyCache new Mapstring, 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而非普通对象Map的has()/get()/set()均为 O(1) 平均复杂度且键可以是任意类型不限于字符串不会像普通对象那样受原型链污染或仅支持字符串键的困扰在 TypeScript 中建议显式声明键值泛型如new Mapstring, string()命中分支用!断言has()已确认存在get()返回值用非空断言!声明避免再次判空保持原函数纯函数语义缓存包装不改变原函数的对外行为仅把计算换成了查表 必要时计算。在 langfuse 源码中可以找到与之一致的手法。以 web/src/features/trace-graph-view/buildStepData.ts 为例它在为 trace 观测数据分配全局时序步骤时先用一个Mapstring, { start: number; end: number }把每条观测的时间戳换算结果缓存起来后续的排序与递归分组都直接复用缓存值避免对同一观测重复执行new Date(obs.startTime).getTime()这类解析成本较高的调用function assignGlobalTimingSteps( data: AgentGraphDataResponse[], ): AgentGraphDataResponse[] { const dataCopy: AgentGraphDataResponse[] []; const timestampCache new Mapstring, { start: number; end: number }(); for (const obs of data) { const obsCopy { ...obs }; dataCopy.push(obsCopy); timestampCache.set(obs.id, { start: new Date(obs.startTime).getTime(), end: obs.endTime ? new Date(obs.endTime).getTime() : new Date(obs.startTime).getTime(), }); } // sort observations by start time const sortedObs [...dataCopy].sort( (a, b) timestampCache.get(a.id)!.start - timestampCache.get(b.id)!.start, ); const stepGroups buildStepGroups(sortedObs, timestampCache); // ... }注意这里timestampCache虽然是在函数内部创建而非模块顶层但它的使用逻辑与规则完全一致先集中写入缓存再在排序与递归buildStepGroups中反复读取把每个时间戳换算一次的承诺贯穿整个算法。它证明该模式的本质是一个作用域内多次读取同一计算结果时用 Map 换时间——作用域可以视数据生命周期从模块级长期共享到函数级单次流程内共享灵活选择。再比如服务端的 web/src/ee/features/billing/server/stripe/stripeBillingService.ts 中也出现了const priceCache new Mapstring, Stripe.Price()同样是在一个处理流程内用 Map 缓存 Stripe 价格对象的查询结果避免对同一 price id 重复请求。核心模式二单值函数的模块级缓存与失效当被缓存的对象只有一个例如当前用户是否登录这种全局布尔状态时不必用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作为未缓存哨兵由于合法的布尔值只有true/false把缓存类型声明为boolean | nullnull专用于表示尚未计算这样即使真实结果是false也能被正确缓存若用if (!isLoggedInCache)判断false会被误判为未缓存而永远重算显式失效接口单值缓存必须考虑状态变化。示例中的onAuthChange()把缓存重置为null下一次调用isLoggedIn()时就会重新读取document.cookie。这是模块级缓存与状态打交道的唯一正确姿势——缓存永远要有对应的失效路径。这引出了使用模块级缓存最重要的原则只有纯函数、或结果在其生命周期内不随外部状态变化的函数才适合无脑缓存。凡是读取document.cookie、localStorage、当前时间、服务端数据这类可变输入要么确保有明确的失效触发点如上面的onAuthChange要么把缓存作用域收窄到单次流程内如buildStepData.ts的函数级缓存。为什么用 Map模块级而不是 Hook规则文件最后特别强调Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.用 Map 而非 Hook这样它在任何地方都可用工具函数、事件处理器而不仅限于 React 组件。这一点至关重要因为它划清了useMemo与模块级缓存的职责边界维度useMemoHook模块级 Map 缓存使用位置仅限 React 组件/自定义 Hook 内任意模块、工具函数、事件处理器缓存生命周期随组件实例组件卸载即释放模块加载后全局存活直到页面卸载失效时机依赖数组变化时自动重算手动失效如onAuthChange或永不失效纯函数典型场景依赖 props/state 的派生数据与组件状态无关的确定性计算slug、解析、格式化langfuse 源码中两种手段并存且分工明确。例如导航过滤逻辑 web/src/components/layouts/app-layout/hooks/useFilteredNavigation.ts 依赖session、routerProjectId、entitlements等会随用户状态变化的值因此用useMemo把过滤上下文 → 过滤路由 → 映射 NavigationItem做成一条缓存链依赖变化才重算// Memoize filter context to prevent unnecessary recalculations const filterContext useMemoNavigationFilterContext( () ({ routerProjectId, routerOrganizationId, session, // ... }), [/* 依赖数组 */], ); // Memoize filtered routes const filteredRoutes useMemo(() { return applyNavigationFilters(ROUTES, filterContext, organization); }, [filterContext, organization]);而像 Markdown 渲染这类输入输出确定、与 React 状态无关的场景langfuse 则采用了模块级常量的方式。在 web/src/components/ui/MarkdownViewer.tsx 中const MemoizedReactMarkdown: FCOptions memo(ReactMarkdown)把react-markdown组件在模块级 memo 化跨父组件重渲染复用同一个组件实例const remarkPluginsDefault [remarkGfm]、remarkPluginsWithPromptRefs以及markdownComponents渲染器映射表全部提升到模块作用域。文件注释web/src/components/ui/MarkdownViewer.tsx明确解释了原因Module-level so custom-element types stay stable across parent re-renders. Inline renderers ... are a new component type every render and remount CodeBlock, dropping local state.提升到模块级使自定义元素类型在父级重渲染间保持稳定内联定义的 renderer 每次渲染都是新组件类型会导致 CodeBlock 重新挂载、丢失复制/滚动等本地状态。这正是规则模块级而非 hook的深层价值模块级引用是稳定的。useMemo的返回值在组件内也是每次渲染一个稳定引用但组件外部的工具函数、模块初始化代码、事件处理器根本无法调用 Hook——此时模块级 Map/常量是唯一选项。与相邻规则的组合运用js-cache-function-results不是孤立的它在js-系列中与多条规则天然互补js-index-maps当同一份数据集合需要按 id 反复查找时先构建一次Map索引替代array.find()的 O(n) 线性扫描。buildStepData.ts中timestampCache同时承担了计算结果缓存与按 obs.id 索引双重职责两者边界常重合js-cache-storage缓存localStorage/sessionStorage读取。注意localStorage读取是可变输入必须像isLoggedIn示例那样设计失效机制如监听 storage 事件js-hoist-regexp把new RegExp(...)提出循环/函数与模块级思想一脉相承——都是把昂贵的、与入参无关的初始化成本上提到模块层server-cache-lru/server-cache-react服务端对应的跨请求 LRU 缓存与每请求React.cache()去重。模块级 Map 是客户端全局缓存的最小实现而服务端需要容量上限与请求隔离因此演进为 LRU 与React.cache()。在 langfuse 的 web/src/features/scores/contexts/ScoreCacheContext.tsx 与 web/src/features/corrections/contexts/CorrectionCacheContext.tsx 中可以看到另一种形态它们用 React Context Map状态new Map(prev)的不可变更新做组件树范围内的缓存。这说明实际工程中缓存作用域是分层的——模块级全局纯计算、Context 级组件树内共享、useMemo级单组件派生值各司其职而本规则聚焦的是最外层、最通用的模块级形态。使用边界与注意事项在动手给函数加缓存前请对照以下检查清单函数必须是确定性的纯函数相同输入必须恒得相同输出。读 cookie、读 localStorage、读系统时钟、依赖全局可变状态的函数都不满足除非你有可靠的失效路径如onAuthChange确认收益确实存在规则文件的初衷是同一输入在渲染中被调用 100 次。如果函数只被调用一次或输入从不重复加缓存只会徒增内存与分支开销——先测量再优化注意内存驻留模块级 Map 的生命周期与页面一致缓存条目永不自动回收。对输入空间无界如用户输入的任意文本、不断增长的 id 集合的缓存要格外谨慎必要时加上容量上限或 LRU 淘汰策略服务端场景可参考server-cache-lru规则失效策略先行任何缓存都必须回答何时失效。纯函数缓存可以永不过期状态驱动的缓存必须提供显式重置入口否则会读到陈旧数据——这是模块级缓存最常见的线上 bug 来源键的类型安全TS 中给Map声明明确的键值泛型如Mapstring, string并注意键的引用相等性——如果键是对象且用内容判等需自行实现规范化的键如JSON.stringify后的字符串不要用它替代useMemo的职责依赖组件 props/state 的派生数据仍应使用useMemo依赖数组驱动失效模块级缓存只负责与组件生命周期无关的确定性计算正如规则所强调的works everywhere: utilities, event handlers, not just React components。总结一套可以直接落地的优化模板将 Vercel 这条规则落地到任何 React/Next.js 项目只需三个步骤识别找出在渲染循环、工具函数或事件处理器中被同输入重复调用的纯函数slugify、正则解析、时间格式化、字段映射等包装多值函数用模块级Map先has后get/set单值函数用模块级变量 null哨兵失效纯函数缓存保持只读共享涉及可变输入的缓存提供显式重置入口。langfuse 仓库中 buildStepData.ts 的时间戳 Map、MarkdownViewer.tsx 的模块级组件/插件常量、useFilteredNavigation.ts 的 useMemo 缓存链分别示范了函数级临时缓存模块级稳定引用组件级派生缓存三种形态可作为本规则的完整对照样本。需要更系统地应用整套规范时可查阅技能总纲 web/.agents/skills/vercel-react-best-practices/SKILL.md57 条规则、8 大优先级类别以及同目录下完整的 57 个规则文件。【免费下载链接】langfuse Open source AI engineering platform: LLM evals, observability, metrics, prompt management, playground, datasets. Integrates with OpenTelemetry, LangChain, OpenAI SDK, LiteLLM, and more. YC W23项目地址: https://gitcode.com/GitHub_Trending/la/langfuse创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考