ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Polar 前端性能优化实战:缓存 Storage API 调用(localStorage / sessionStorage / Cookie)

Polar 前端性能优化实战:缓存 Storage API 调用(localStorage / sessionStorage / Cookie) Polar 前端性能优化实战缓存 Storage API 调用localStorage / sessionStorage / Cookie【免费下载链接】polarPolar — A billing platform for the intelligence era项目地址: https://gitcode.com/GitHub_Trending/po/polar导读本文聚焦 Polar 前端工程clients/apps/web在 React 性能最佳实践中的一个高频问题localStorage、sessionStorage与document.cookie属于同步且昂贵的 I/O 操作在渲染、工具函数、事件处理器中被反复读取会带来可感知的性能损耗。你将掌握内存 Map 缓存 外部变更失效的完整方案并看到 Polar 仓库中 useLocalStorage.ts 的真实实现如何落地这一规则。本文内容以 clients/apps/web/.agents/skills/vercel-react-best-practices/rules/js-cache-storage.md 规则文档为主体属于该仓库维护的 React Best Practices 规则集js-前缀对应 JavaScript Performance 分区详见 README.md文档声明的impact: LOW-MEDIUM核心收益是减少昂贵 I/Oreduces expensive I/O。为什么 Storage API 调用是昂贵的浏览器存储 API 的三个入口都有相同的性能特征API特性典型代价localStorage同步读取无异步版本每次getItem都是磁盘/持久化层 I/OsessionStorage同步读取会话级隔离同上且随页面生命周期反复命中document.cookie同步解析整段 Cookie 字符串每次访问都要重新解析分号分隔的键值串与 React 状态、变量访问不同这些 API 没有引擎层缓存——每次调用都会真实触发一次底层存储读取。因此在热路径渲染函数、事件处理器、工具函数中每次调用都重新读存储的模式会把一次本可避免的 I/O 放大成 N 次。规则文档中的反例非常直观function getTheme() { return localStorage.getItem(theme) ?? light } // Called 10 times 10 storage reads同一个getTheme()被调用 10 次就会产生 10 次存储读取。修复思路是把读到的值缓存在内存中让后续调用直接命中内存。正确做法用模块级 Map 做内存缓存规则文档给出的推荐实现是一个模块级Map而不是 React Hook。原因是 Hook 只能在组件里使用而存储读取通常发生在工具函数、事件处理器、路由守卫等非组件环境中——模块级缓存才能到处可用const storageCache new Mapstring, string | null() function getLocalStorage(key: string) { if (!storageCache.has(key)) { storageCache.set(key, localStorage.getItem(key)) } return storageCache.get(key) } function setLocalStorage(key: string, value: string) { localStorage.setItem(key, value) storageCache.set(key, value) // keep cache in sync }要点拆解首次读取时才填充缓存has(key)判定保证每个 key 至多触发一次真实getItem写入时同步更新缓存setLocalStorage在调用setItem后立即storageCache.set保证缓存与存储保持一致keep cache in sync缓存语义Mapstring, string | null中null用于表示key 不存在这样getItem返回null的结果同样可以被缓存避免对不存在的 key 反复发起 I/O。这条规则与同规则集下的 js-cache-function-results.md缓存重复函数调用结果和 js-cache-property-access.md循环内缓存属性访问一脉相承用内存换 I/O是 JavaScript 性能优化的通用手段。Cookie 读取的缓存一次解析多次复用document.cookie与localStorage不同它没有按 key 读取的 API——每次访问返回的是整段k1v1; k2v2字符串。因此更高效的做法是解析一次、整体缓存let cookieCache: Recordstring, string | null null function getCookie(name: string) { if (!cookieCache) { cookieCache Object.fromEntries( document.cookie.split(; ).map((c) c.split()), ) } return cookieCache[name] }实现说明split(; )按分号拆出各键值对再split()拆出 key 与 valueObject.fromEntries把键值对数组转换为普通对象之后每次getCookie(name)都是纯内存的O(1)属性查找cookieCache用null作为未初始化哨兵只解析一次。该模式也出现在同规则集的 js-cache-function-results.md 中其单值缓存示例同样用document.cookie.includes(auth)判断登录态并配套onAuthChange()在鉴权状态变化时把缓存重置为null——这正对应下面要讲的缓存失效问题。缓存失效外部变更时必须失效内存缓存最大的风险是与真实存储脱节。以下场景会使缓存过期其他标签页写入浏览器原生的storage事件会在其他标签页修改存储时触发服务器设置的 CookieSet-Cookie由服务端下发页面脚本无法感知页面从后台恢复长时间挂起期间存储可能已被改动。规则文档给出两层失效策略window.addEventListener(storage, (e) { if (e.key) storageCache.delete(e.key) }) document.addEventListener(visibilitychange, () { if (document.visibilityState visible) { storageCache.clear() } })storage事件只在其他标签页改动时触发注意当前标签页自身的写入不会触发它事件对象的key告诉我们哪个 key 变了精准删除对应缓存项visibilitychange页面重新变为可见时说明可能经历了不可见的挂起期此时整表clear()是最稳妥的全量失效。这一监听外部变更 按需失效的设计正是 Polar 仓库中真实代码的运行基础。仓库实证Polar 的 useLocalStorage 如何落地该规则规则文档给的是通用范式Polar 前端在 clients/apps/web/src/hooks/useLocalStorage.ts 中把它实现成了一个可复用的useState形态 Hook值得逐行对照学习。1. 模块级 Map 缓存与规则文档完全同构const cache new Mapstring, { raw: string | null; value: unknown }()与规则文档的Mapstring, string | null相比Polar 的缓存值多存了一份raw原始字符串。readFromStorage里先用当前 raw 与缓存 raw 是否一致做短路判断const cached cache.get(key) if (cached cached.raw raw) return cached.value as T这样做的精妙之处在于JSON.parse每次都会产生新的对象引用如果直接返回解析结果useSyncExternalStore会把每次快照都视为值变化从而死循环。缓存保证底层字符串没变返回的引用就不变保持Object.is稳定。注释原话是Without this,useSyncExternalStorewould treat each JSON.parse result (or any deserialised object) as a new value and infinite-loop.2. 原生 storage 事件 自定义事件的订阅组合const subscribe (callback: () void): (() void) { if (typeof window undefined) return () {} window.addEventListener(storage, callback) window.addEventListener(CHANGED_EVENT, callback) return () { window.removeEventListener(storage, callback) window.removeEventListener(CHANGED_EVENT, callback) } }这里补上了规则文档未展开的一个关键事实原生storage事件只在其他标签页触发同一标签页内的写入不会触发它。所以 Polar 自定义了一个polar:local-storage-changed事件在setValue写入后手动window.dispatchEvent(new Event(CHANGED_EVENT))让同标签页的其他 Hook 实例也能同步——跨标签页用原生事件同标签页用自定义事件各司其职。3. 写入后同步失效缓存const setValue useCallback( (next: T) { try { window.localStorage.setItem(key, serialize(next)) } catch { // ignore — dispatch anyway so subscribers re-read and stay consistent } cache.delete(key) window.dispatchEvent(new Event(CHANGED_EVENT)) }, [key, serialize], )注意这里与规则文档写入后cache.set保持同步的差异Polar 选择cache.delete(key)让下次读取时重新走一次完整解析。这样即使setItem失败隐私模式、配额超限订阅者也会重新读取存储保证 UI 与真实存储一致——失效比乐观更新更安全。4. SSR 安全与容错const readFromStorage T(...): T { if (typeof window undefined) return defaultValue let raw: string | null try { raw window.localStorage.getItem(key) } catch { return defaultValue } ... }服务端typeof window undefined直接返回defaultValue避免水合不匹配getItem/setItem全程try-catch兼容 Safari/Firefox 隐私模式与配额超限抛异常的场景。这与同规则集的 client-localstorage-schema.md 强调的 Always wrap in try-catch 完全一致。仓库中的另一处落地导航未读角标同样的模式还出现在 clients/apps/web/src/components/Organization/HumanReviewCase/appealCaseUnread.ts 中以polar:appeal-case-seen:${organizationId}为 key 存储已读计数读取走getSeenCount写入走markSeensubscribe同时监听自定义事件SEEN_EVENT与原生storage事件让导航角标在多标签页与同标签页内都能实时刷新通过useSyncExternalStore把存储值接入 React 渲染。而 CookieConsent.tsx 中的cookieConsentGiven()则展示了 Cookie/存储读写的另一项配套纪律先判typeof window undefined再访问读取结果直接用于 React 状态避免水合不一致。这两个文件可以作为规则落地到真实业务组件的对照样本。配套规则版本化与最小化存储缓存只是读得快要让存储长期健康规则文档集还提供了 client-localstorage-schema.mdimpact: MEDIUM作为配套纪律key 加版本前缀如userConfig:v2避免 schema 演进冲突迁移时读取旧版本 key、转换、写入新版本、删除旧 key只存 UI 需要的字段从 20 字段的完整用户对象里只挑theme、notifications等子集减小存储体积防止误存 Token/PII/内部标志全程 try-catchgetItem/setItem在隐私模式、配额超限、存储被禁用时都会抛异常。把这条规则与本文的缓存规则叠加就是 Polar 前端存储层的完整策略按版本写小数据、全程容错、内存缓存加速、外部变更失效。实践检查清单编写或评审涉及浏览器存储的代码时逐条核对是否在热路径渲染、事件处理器、工具函数反复调用localStorage.getItem/document.cookie是否用模块级Map而非 Hook缓存读取结果保证非组件环境也可用Cookie 是否只解析一次Object.fromEntries而非每次全量解析写入后是否同步了缓存set或delete是否监听了storage事件跨标签页与visibilitychange页面恢复以失效缓存同标签页多实例同步是否通过自定义事件实现原生storage事件在同标签页不触发读取是否做了 SSR 守卫typeof window undefined与try-catch容错存储 key 是否带版本号、数据是否最小化以上每一项都能在 rules/js-cache-storage.md、src/hooks/useLocalStorage.ts 与 rules/client-localstorage-schema.md 中找到对应实现依据。【免费下载链接】polarPolar — A billing platform for the intelligence era项目地址: https://gitcode.com/GitHub_Trending/po/polar创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表