Cal.com 前端性能实践:缓存 localStorage / sessionStorage / Cookie 同步读取(js-cache-storage 规则详解) Cal.com 前端性能实践缓存 localStorage / sessionStorage / Cookie 同步读取js-cache-storage 规则详解【免费下载链接】cal.diyScheduling infrastructure for absolutely everyone.项目地址: https://gitcode.com/GitHub_Trending/ca/cal.diy本文详解 Vercel React Best Practices 规则库中js-cache-storageCache Storage API Calls规则的核心原理为什么localStorage、sessionStorage和document.cookie的同步读取属于昂贵 I/O如何用模块级 Map 缓存把重复读取降为内存访问以及如何通过storage与visibilitychange事件正确失效缓存。文中给出的全部代码模式均来自规则文档并结合 Cal.com 仓库中 时钟显示模块、最近模拟记录 与 安全存储封装 的真实实现加以印证。规则定位在 45 条性能规则中的位置该规则的完整定义位于 js-cache-storage.md其 frontmatter 元信息如下标题Cache Storage API Calls影响等级LOW-MEDIUMimpactDescription: reduces expensive I/O即减少昂贵 I/O标签javascript、localStorage、storage、caching、performance从 SKILL.md 的规则分类表看这条规则属于第 7 类JavaScript PerformanceLOW-MEDIUM与js-cache-function-results、js-cache-property-access、js-index-maps等同属计算/访问成本削减方向它排在 Eliminating Waterfallsasync-CRITICAL与 Bundle Sizebundle-CRITICAL之后。这意味着它不会像消除瀑布请求那样带来数量级的提升但在高频调用场景渲染循环中反复读存储下可以稳定削减同步 I/O 开销。问题本质存储读取是同步且昂贵的规则文档给出的核心结论一句话概括localStorage、sessionStorage和document.cookie都是同步 API读取成本高。应将读取结果缓存在内存中。反模式示例每次调用都触发一次存储读取function getTheme() { return localStorage.getItem(theme) ?? light } // Called 10 times 10 storage reads在列表渲染、每个组件各自取一次配置等场景下调用 10 次 10 次存储读取会成倍放大。Cal.com 仓库中就有这类真实场景useTheme 每次执行时都会执行localStorage.getItem(app-theme)见packages/lib/hooks/useTheme.ts第 16 行而getTheme正是文档反例所使用的函数名说明该模式在主题、时钟偏好这类多处调用、值稳定的场景中最为常见。另一个真实反例是 recentImpersonations.ts// packages/lib/recentImpersonations.ts export function getRecentImpersonations(): RecentImpersonation[] { try { const stored localStorage.getItem(STORAGE_KEY); // 每次调用都读一次存储 if (!stored) return []; return JSON.parse(stored); } catch { return []; } }该函数每次调用都会执行一次getItem加一次JSON.parse。在低频操作记录最近模拟过的用户中尚可接受但若同类函数出现在渲染路径上就是规则文档所警示的模式。标准模式模块级 Map 缓存规则文档给出的正确写法是Map 缓存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 }这里有三个关键设计点缓存粒度是 key。Mapstring, string | null显式允许缓存null值——注意getItem对不存在的 key 返回null如果只缓存有值的 key不存在的 key 会每次穿透到存储缓存对这类 key 完全失效。用has()而非get()判空正是为了区分未查过和查过但不存在。写路径必须同步缓存。setLocalStorage在写存储后立即storageCache.set(key, value)保证同一模块内的读写一致性。任何绕过该封装直接调用localStorage.setItem的代码都会破坏这一不变式。用模块级变量而不是 Hook。规则文档明确说明使用 Map而非 Hook使其在工具函数、事件处理器等任意位置都可用而不限于 React 组件内。这与同目录 js-cache-function-results.md 的结论一致——该规则同样要求module-level Map并强调 Map 方案可以覆盖非组件上下文。Cal.com 仓库中已经存在该模式的真实落地形态clock.ts 用模块级timeOptions对象把 localStorage 中的 24 小时制偏好与时区偏好读入内存// apps/web/lib/clock.ts节选 const timeOptions: TimeOptions { is24hClock: false, inviteeTimeZone: , }; const initClock () { if (isInitialized) { return; } if (getIs24hClockFromLocalStorage() null) set24hClock(isBrowserLocale24h()); timeOptions.is24hClock !!getIs24hClockFromLocalStorage(); timeOptions.inviteeTimeZone localStorage.getItem(timeOption.preferredTimeZone) || CURRENT_TIMEZONE; }; const set24hClock (is24hClock: boolean) { setIs24hClockInLocalStorage(is24hClock); timeOptions.is24hClock is24hClock; // 写入存储的同时更新内存缓存 };is24h()/timeZone()读取时优先命中timeOptions内存值写入时set24hClock/setTimeZone同步更新内存——这与规则文档read from memory, write-through to cache的封装完全同构。差异在于它用单对象缓存固定 key而文档的 Map 版本可缓存任意 key两者按场景取舍。Cookie 缓存一次性解析整条 document.cookiedocument.cookie的读取成本更典型每次读取都要解析整个字符串。文档给出的模式是懒加载整表缓存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] }要点用单一布尔哨兵null/对象而非 Map 的has()因为 Cookie 表要么整表解析、要么完全未解析不存在部分缓存状态split(; ).map(c c.split())假设 Cookie 值中不含分隔冲突的简单结构遇到带 URL 编码或的值时split()会产生多于两个元素此时应在map中改用c.split()的[key, ...value]拆法自行加固此为基于代码结构的推断文档未展开Cookie 由服务端 Set-Cookie 更新因此 Cookie 缓存天然比 localStorage 缓存更易失鲜必须配合下一节的失效策略。缓存失效外部变更的两种触发源文档特别标注了Important部分——如果存储可能被外部修改另一个标签页写入、服务端下发的 Cookie必须主动失效缓存否则会读到过期值window.addEventListener(storage, (e) { if (e.key) storageCache.delete(e.key) }) document.addEventListener(visibilitychange, () { if (document.visibilityState visible) { storageCache.clear() } })两段监听的分工是明确的storage事件只在其他标签页/窗口修改同一 origin 的存储时触发且带有e.key被修改的 key。因此可以精确delete(e.key)单条失效e.key为null表示执行了clear()此时保守做法是清空整个storageCache。visibilitychange事件标签页重新变为可见时整体clear()。这覆盖了后台标签页期间被其他来源包括服务端 Cookie、同一浏览器的其他标签页修改以及storage事件不可靠的边缘情况代价只是可见性恢复后重新读一次存储。这两条规则共同回答了缓存方案最常被质疑的问题内存缓存与真实存储脱节怎么办——精确失效 粗粒度兜底双层设计。与安全存储封装的组合Cal.com 的做法值得注意的是Cal.com 并没有裸用localStorage而是通过 webstorage.ts 提供了带 try/catch 的安全封装// packages/lib/webstorage.ts节选 export const localStorage { getItem(key: string) { try { return window.localStorage.getItem(key); } catch { // In case storage is restricted. Possible reasons // 1. Third Party Context in Chrome Incognito mode. return null; } }, setItem(key: string, value: string) { /* 同样的 try/catch 降级 */ }, removeItem: (key: string) { /* 同样的 try/catch 降级 */ }, };文件头注释说明其动机在第三方 iframe嵌入的预订页或 Chrome 无痕模式的第三方上下文里window.localStorage访问会直接抛异常封装层将其降级为读返回null、写静默失败。仓库中 useTheme、clock.ts 等模块全部import { localStorage } from calcom/lib/webstorage而非裸 API。将文档的缓存模式叠加在这层封装之上就得到兼顾少读与安全读的完整形态在文档模式基础上把底层读取替换为安全封装import { localStorage } from calcom/lib/webstorage; const storageCache new Mapstring, string | null(); function getLocalStorage(key: string) { if (!storageCache.has(key)) { storageCache.set(key, localStorage.getItem(key)); // 受限环境下安全地返回 null } return storageCache.get(key); } function setLocalStorage(key: string, value: string) { localStorage.setItem(key, value); storageCache.set(key, value); // keep cache in sync }这样在嵌入场景下null只会被真实读取一次并缓存同时保留后续标签页写入后通过storage事件失效的能力。落地清单结合规则文档与仓库实现在 Cal.com 前端代码中应用该规则时可对照以下要点识别高频读凡是渲染路径、列表循环、多处组件各自执行的localStorage.getItem/document.cookie读取优先考虑缓存对照反例recentImpersonations.ts 的每次调用都getItemJSON.parse封装成 get/set 对按 js-cache-storage.md 的 Map 模式提供getLocalStorage/setLocalStorage写路径同步缓存并用has()判断以正确缓存null用模块级变量而非 Hook保证工具函数与事件处理器中同样可用加两层失效storage事件按 key 精确失效visibilitychange可见时整体clear()服务端 Cookie 场景尤其依赖后者底层走安全封装Cal.com 的读取应基于 webstorage.ts 的localStorage/sessionStorage导出以兼容嵌入式 iframe 与无痕模式的受限存储环境固定少量 key 的场景可参考 clock.ts 的单对象初始化缓存initClock 写时同步结构更简单、无 Map 开销。该规则的影响等级为 LOW-MEDIUM收益集中在昂贵 I/O 削减在大型应用中应与同类的 js-cache-function-results模块级 Map 缓存纯函数结果配合使用共同压降渲染期的重复计算与重复存储访问。【免费下载链接】cal.diyScheduling infrastructure for absolutely everyone.项目地址: https://gitcode.com/GitHub_Trending/ca/cal.diy创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考