Chrome Extension 里缓存已观察数据该放哪里
最近给一个 Jenkins 页面写 Chrome Extension,最开始只是想把构建历史表格整理得清楚一点:谁触发、什么时候触发、用了什么参数、关联了哪些 Git 变更。
这次 Jenkins 部署的页面只显示最近 7 条构建,新的构建一出现,最老那条就掉出窗口;旧构建 URL 也不一定还能打开。这是当前页面和保留配置的约束,不是 Jenkins 一律只能保存 7 条。扩展不能恢复服务端已经清理掉的数据,但可以记住自己已经看过的构建。
于是问题变成了:这份「已观察构建记录」应该存在哪里?
候选项很多:页面 localStorage、chrome.storage.local、IndexedDB、storage.session、内存变量,甚至还有 storage.sync、Cache Storage、OPFS 这类容易被顺手想到的选项。它们都能存东西,但归属、可见性、生命周期和查询能力完全不一样。
这个 Jenkins enhancer 最后选了 chrome.storage.local,同时需要让用户看清缓存来源,并提供查看和管理缓存的入口。
先看这份数据属于谁
这份记录由扩展从已打开的 Jenkins 页面采集,是一份「扩展观察日志」。它记录本机看过什么,与服务端保留哪些构建是两套生命周期。
这个归属判断会直接影响存储选择:
- 如果数据属于页面业务本身,页面
localStorage很自然。 - 如果数据属于扩展能力本身,
chrome.storage.local更自然。 - 如果数据已经变成大量结构化历史,需要索引、分页和复杂查询,
IndexedDB才开始有明显优势。
Chrome 官方 chrome.storage 文档把它描述成 extension-specific 的持久化方式,并且明确 content scripts 默认可以访问。官方同一页也提醒,content script 里用 Web Storage 会共享宿主页存储。因此,content script 直接写 window.localStorage,写进去的是 Jenkins 这个站点的 origin,而不是扩展自己的 origin。

页面 localStorage 的优势是真的
一开始我对页面 localStorage 有点偏见,觉得它像是在「污染页面」。但这个理由在当前场景里并不够扎实。这个扩展本来就只服务 Jenkins 的两个站点,写一份带前缀的缓存 key,并不会立刻造成工程灾难。
而且页面 localStorage 有一个很真实的优势:透明。
用户打开 Jenkins 页面的 DevTools,在 Application 面板里就能看到 localStorage。如果缓存里出现了奇怪数据,用户可以自己确认,也可以自己删掉。这种可见性对内部工具很重要。尤其这个缓存是为了弥补 Jenkins UI 的缺口,如果它自己又变成一个不可见的小黑盒,体验上会打折。
MDN 的 localStorage 文档说明它按 document origin 存储,且跨浏览器会话保留。放到 Jenkins enhancer 里,会带来这些结果:
- 测试环境和正式环境会天然分开,因为它们是不同 origin。
- 数据跟 Jenkins 站点数据在一起,用户清理站点数据时会一起清掉。
- Jenkins 页面自己的脚本也能看到这些 key。
- 值只能是字符串,复杂对象需要自己
JSON.stringify()和JSON.parse()。 - API 是同步的,少量数据问题不大,大量读写会阻塞主线程。
缓存只服务某个站点、数据量小、调试透明度优先,并且不需要跨扩展页面共享时,页面 localStorage 很合适。
这次我希望把构建缓存和 Dashboard 收藏、最近访问、缓存清理入口放进同一套扩展设置,所以选了扩展存储。页面 origin 与扩展 ID,决定的是两种不同的管理范围。
chrome.storage.local 更像扩展自己的缓存
chrome.storage.local 的优势在于归属清楚。它绑定扩展 ID,而不是 Jenkins 页面 origin;content script、popup、options page、service worker 都可以围绕同一份扩展数据工作。官方文档还列了几个对扩展很友好的特性:异步读写、JSON 可序列化对象、onChanged 监听、getBytesInUse() 统计空间、setAccessLevel() 控制 content script 访问级别。
生命周期也更符合「插件缓存」:storage.local 存在本机,扩展更新、reload、Chrome 重启都不应该清掉;扩展卸载时会清掉。官方文档写到,storage.local 默认上限是 10 MB,Chrome 113 及以前是 5 MB,需要更大空间时可以申请 unlimitedStorage。
这里有一个本地开发边界:chrome.storage.local 跟扩展 ID 绑定。如果换目录重新 Load unpacked,或者没有稳定的开发 ID,新扩展可能读不到旧扩展的数据。Chrome manifest key 文档专门解释了如何在开发阶段保持一致的 extension ID。真正要长期分发时,这件事应该提前定好。
透明度这点以前确实弱一些。现在 Chrome 已经补上了入口:Chrome DevTools 从 Chrome 132 开始支持查看和修改 extension storage。在页面 DevTools 的 Application 面板里,可以展开 Extension Storage,查看运行在当前页面上的扩展存储。这个入口仍然不如页面 localStorage 直觉,但已经不是完全看不到。
这个场景里,当前实现简化后大概是这样的:
const createCacheKey = (context: JenkinsPageContext) => {
const url = new URL(context.href);
return `${CACHE_KEY_PREFIX}:${context.environment}:${url.origin}:${createJobCacheScope(context.href)}`;
};
const saveObservedBuildRows = async (context: JenkinsPageContext, rows: BuildHistoryRow[]) => {
const key = createCacheKey(context);
const cachedRows = await loadCachedBuildRows(context);
const nextRows = mergeAndLimit(rows, cachedRows);
await chrome.storage.local.set({ [key]: nextRows });
return nextRows;
};缓存 key 里有三层含义:
environment:测试环境和正式环境隔离。origin:不同 Jenkins 站点隔离。job scope:同一个 job 从直达 URL 或 view 列表进入时共享缓存。
这比页面 localStorage 多了一点实现成本,但也换来一个好处:隔离和共享都由扩展主动定义,而不是完全交给页面 origin。
IndexedDB 是数据库,不是默认缓存
如果只看 Web 平台能力,web.dev 的 Storage for the web会给出很明确的推荐:资源用 Cache Storage,文件内容用 OPFS,其他数据优先 IndexedDB。MDN 的存储配额文档也把 IndexedDB 放在「大型结构化数据、可索引查询」的位置。
这套建议放在普通 Web App 里很合理。IndexedDB 支持更大的数据量、事务、object store、索引和结构化克隆。存几万条记录、按时间范围查、按 job / branch / appName 建索引,IndexedDB 会比把一个大数组塞进 chrome.storage.local 更合适。
但在 Chrome Extension content script 里,还要多看一层 context。Chrome 的 Storage and cookies 文档说明,扩展自己的存储会在 extension origin 下共享;content script 里调用 Web 平台存储 API 时访问的是宿主页数据。IndexedDB、Cache Storage 这类 Web 平台存储在 extension service worker、extension page、offscreen document 里可以作为扩展自己的存储使用,但 content script 里直接调用时,心智上不能把它当作扩展统一数据库。
所以在当前场景里认真使用扩展自己的 IndexedDB,比较稳的形态会是:

对每个 job 只缓存几十条记录的需求,这条链路引入的 schema 版本、事务、消息通信和错误恢复成本偏高。等记录量和索引查询需求增长后,再承担这套数据库维护更划算。
Stack Overflow 上也有类似讨论:chrome.storage.local 和 IndexedDB 的选择主要取决于数据形状。社区里常见的判断是:简单 JSON 和少量状态用 chrome.storage.local,大数组、复杂对象、索引查询和高频局部更新再考虑 IndexedDB。这个讨论不能替代官方文档,但它很好地补上了工程经验层面的取舍。
其他选项可以很快排掉
chrome.storage.session 适合临时状态。官方文档说明它在扩展 reload、禁用、更新和浏览器重启时都会清掉。用它记录「本次页面生命周期内已经处理过哪些 DOM 节点」可以,用它弥补 Jenkins 历史窗口不合适。
chrome.storage.sync 适合用户设置,不适合本地构建历史缓存。它有约 100 KB 总量和 8 KB 单项限制,还会跟随 Chrome 账号同步。Jenkins 构建记录是机器本地观察结果,不应该默认同步到另一台机器。
chrome.storage.managed 是企业策略配置,读多写少,甚至对扩展本身是只读。它可以用来给全员下发配置,不适合存用户本地观察结果。
Cache Storage 适合 HTTP request / response 缓存,OPFS 适合文件型数据。它们都不是「几行构建摘要」的第一选择。
Cookies 更不合适。符合发送条件的 cookie 会放进请求头,既增加请求体积,也把本来只用于本地 UI 的构建摘要带到网络层。
内存变量则只适合当前页面运行期。刷新页面、重新注入 content script 或关闭浏览器都会丢。它可以配合防抖、避免重复渲染,但不能作为历史缓存。
第三方生态怎么处理这个问题
第三方框架也能反映开发者心智。Plasmo 的 Storage API 文档把扩展持久化封装成一个跨 background service worker、content script、extension page 的 Storage 抽象,并支持选择 local / sync 等 storage area。它还提供把指定 key 复制到 Web localStorage 的能力。
这套设计默认把状态放在扩展存储里;只有确实需要页面可见、页面共享或和网页脚本协作时,才显式桥接到 localStorage。
Stack Overflow 上关于 window.localStorage 和 chrome.storage.local 的经典讨论也基本是这个方向:选择取决于扩展要做什么。localStorage 的域名隔离和 DevTools 可见性是真优势;chrome.storage.local 的扩展级中心化、content script 可用、跨页面共享和变化监听,也是真优势。
存储选型首先要确定:这份数据应该跟站点走,还是跟扩展走。
回到这个 Jenkins enhancer
这个缓存有几个约束:
- 数据来源只来自当前浏览器已经看过的 Jenkins 页面。
- 缓存不能恢复 Jenkins 从未展示过、已经清理掉的构建。
- 当前 Jenkins 可见行优先,本地缓存只补掉出窗口的旧行。
- 运行中构建不缓存,避免保存过期进度和取消链接。
- 参数值要脱敏,至少隐藏
token、cookie、password、secret、credential这类字段。 - 每个 job 只保留有限条数,避免本地无限膨胀。
这组约束决定了 chrome.storage.local 是当前更合适的默认方案。它把缓存归到扩展,能和收藏、最近访问、缓存清理入口共用一套存储,也能在未来从 content script 之外的页面统一管理。
选择扩展存储后,透明度仍需要由产品补足。不能只在实现里写「存在扩展存储里」,还要让用户在 UI 上看清缓存来源和管理入口:
- 表头显示「当前页面可见 N 条,本地缓存 M 条」。
- 缓存行标记「本地缓存」。
- 提供「查看缓存 JSON」入口。
- 提供「清空当前 job 缓存」入口。
- 需要时再提供「导出缓存」。
这样普通用户不用知道 DevTools 里 Extension Storage 在哪里,也能知道这些记录来自哪里、能不能信、怎么清掉。
以后怎么判断
以后再遇到 Chrome Extension 存储选型,我会按这几个问题判断:
- 数据属于页面还是扩展。页面业务状态优先页面存储;扩展状态优先扩展存储。
- 数据是否要跨站点、跨页面、跨扩展上下文共享。需要共享时优先
chrome.storage.local。 - 用户是否需要直接在页面 DevTools 里看到。透明度特别重要、且数据只服务当前站点时,页面
localStorage可以接受。 - 数据是否需要索引、分页和局部更新。需要数据库能力时再上扩展 context 里的 IndexedDB。
- 数据是否只是当前运行期临时状态。临时状态用内存或
storage.session,不要伪装成持久缓存。
这次选择 chrome.storage.local,依据是数据归属、体量和共享范围。选定存储后,还要把来源标记、查看和清理入口做出来,让用户知道扩展记下了什么,也能自行管理这份记录。