全部笔记

静态资产版本化:长缓存必须建立在不可变 URL 上

用内容哈希协调 HTML 与资源更新,避免新页面引用已删除文件。

把 /app.js 设置为一年缓存,下一次上线仍然使用同一个 URL,用户可能继续运行旧脚本。长缓存并不是单独的响应头优化,它要求资源 URL 与内容一起变化,才能让浏览器自然区分版本。

本文讨论构建生成的 JS、CSS、字体和图片。HTML 与 API 数据通常更新语义不同,不应直接套用相同策略。

内容哈希让 URL 描述版本

GET /assets/search.a1b2c3.js

HTTP/1.1 200 OK
Cache-Control: public, max-age=31536000, immutable

如果文件内容变化,构建器产生新名称,HTML 引用新地址;旧地址保持原内容。immutable 表达缓存期间资源不变,但实际维护必须遵守这个承诺,不能手工覆盖同名文件。

哈希名称也不是加密保护,不能把它当成私有 URL。只要页面引用资源,访客就能看到它。草稿内容、内部配置与秘密不应因为用了随机文件名就放进公开资产目录。

两个部署生成完全相同 CSS,可以共享同一内容哈希地址;部署版本不同不代表每个资产必须重新命名。相反,文件内容改变而部署名称不变,也应该得到新资源身份。内容哈希帮助去重与长缓存,部署标识帮助关联完整页面集合,两者服务不同问题。不要把随机构建时间拼到所有资源名中,导致相同字节每次都重新下载。

哈希由构建器产生,不能手工修改文件后继续保留旧名。若托管层再次压缩或转码,需要明确 URL 表达的是源内容还是最终表示,并按平台支持配置验证器。哈希文件名不是权限保护,也不一定是完整性校验机制;浏览器不会仅根据文件名推导下载字节正确。需要完整性保护时使用相应标准与部署验证。

一个资产清单可以记录逻辑名称、哈希路径、大小和引用关系。HTML 与清单来自同一构建,才能避免新页面引用上一版路径。清单也帮助识别孤立资源,但删除需要考虑旧页面和历史部署,不是“当前没有引用”就立刻清理。资源生命周期比当前仓库引用更长。

部署需要考虑旧 HTML 的生命期

用户可能从浏览器缓存、CDN 或已打开标签页拿到旧 HTML。它仍会请求旧哈希资源,如果部署时立即删除旧文件,就会出现加载失败。原子部署平台常通过版本化产物处理这一问题,但具体保留与切换行为要核实。

自建上传流程更容易出错:先上传新 HTML,资源尚未完成;或先删除旧资源,旧页面还在缓存。安全顺序应保证新 HTML 引用的资源已存在,并保留旧版本需要的资源,直到其合理存活期结束。

自建对象存储流程可先上传全部新资产,验证可获取后再更新 HTML,并保留旧资产。这能避免新 HTML 先引用缺失文件,却不能自动保证多页 HTML 同时切换。若首页和文章页更新分两步,期间可能存在混合版本,需要应用兼容或平台版本路由能力。不要把安全上传顺序称为所有请求天然原子。

版本化部署平台可能把页面与资产作为一个版本切换,但不同平台对于历史资产和边缘缓存有具体保证。应查明旧资源在新版本入口上是否仍可请求,而不是只知道后台保留了旧版本。客户端旧标签页可能请求当前域名下的旧 chunk,历史部署还在另一个地址并不一定帮助它。真实跨版本测试才能验证。

缓存规则还需要区分缺失资源响应。新资源上传传播期间若被请求并缓存为四百零四,随后上传成功也可能不可见。避免先引用未就绪资源,并设置适当错误缓存与清理路径。发布检查确认每个新引用可获取后才切换页面,减小这一窗口。只清首页不能修复一个缺失 chunk 缓存。

CSS 与字体也有版本耦合

新 CSS 引用新字体路径,旧 CSS 仍应能够加载旧字体。不能只关注主 JS。动态导入的 chunk 更容易在用户长时间打开页面后失败,因为它可能直到点击某个按钮才请求。

此时可以提供明确刷新提示,但不能无限自动刷新导致循环。错误监控应记录资源地址与页面版本,方便判断是不是版本不一致,而不是把所有 chunk 加载失败都归因于网络。

用户打开页面后几小时才点击搜索,浏览器此时才请求动态 chunk。如果旧资源已删除,就会出现功能失败而正文仍正常。错误提示可以建议刷新,但先保留任务输入,并避免自动刷新循环。若刷新后仍失败,应表达真实错误,而不是每次再次刷新。资源失败可能是网络问题、扩展阻断或版本缺失,日志需要版本与地址帮助分类。

对涉及写入的交互,刷新不能丢弃未保存内容。可以保存非敏感草稿或提供复制路径,具体依赖产品需求。个人博客搜索较容易恢复,但通用加载错误组件可能被其他表单复用,不应默认强制重载。版本兼容策略应从用户任务出发,不让资源管理决定用户必须损失输入。

资源路径的引用还可能来自 CSS、worker 脚本、wasm 和字体,不只 HTML 中的 script。构建验收可以解析清单与资源依赖,确认文件存在。手工只检查主 JS 会漏掉深层依赖。第三方外部库采用固定版本地址,避免供应商覆盖同一路径导致不可变承诺失效。

HTML 缓存需要另一套节奏

HTML 可以使用较短新鲜期或验证,让新发布及时可见。CDN 与浏览器的缓存控制可以分开,但要查平台支持,避免误以为一条头能独立控制所有层。紧急修复时应同时考虑 HTML、Service Worker 和 CDN 对象。

Service Worker 若存在,会引入额外资源生命周期。预缓存清单、激活时机和旧缓存清理需要测试,不能因为网络层已经版本化就认为离线缓存不会影响发布。

预缓存清单、安装完成、等待激活和旧缓存清理可能分布在不同阶段。立即激活新 worker 可能接管旧页面,让旧页面请求新策略;延迟激活则让新功能不立即可见。需要根据应用兼容性选择,不把 skipWaiting 当成通用正确答案。对于不需要离线功能的技术博客,不添加 service worker 也能减少一层生命周期。

离线缓存中的 HTML 也会引用旧哈希资源,清理旧 cache 时应保证活跃页面仍有可用资源,或提供明确更新流程。网络策略、缓存策略与版本清单要一致。新 worker 安装失败时保留旧能力,而不删除全部缓存后才发现新资源不可用。更新是状态转换,不是简单清空。

紧急安全修复需要评估旧离线代码是否仍可运行,服务器侧授权始终独立生效。不能期待清理 CDN 立即清理每个用户设备。公开资产可能长期保存,因此秘密不进入其中,敏感操作不依赖客户端版本判断。这是不可变资源策略之外必须保留的安全边界。

资源保留是一项兼容政策

历史文件保留多久应由活跃旧页面、离线能力和支持范围决定。个人博客可能只需覆盖常见缓存与标签页周期,业务应用可能需要更长版本兼容。不能把一个固定一年 max-age 直接解释为必须保留恰好一年,也不能在第二天删除而不考虑未请求过的动态依赖。政策需说明哪些旧版本仍受支持,超过范围时如何恢复用户任务。

清理前生成待删除资源与引用版本清单,再检查当前和仍受支持版本。共享资产可能被多个部署复用,仅按文件创建时间删除会误伤当前版本。删除操作使用精确路径和可恢复归档,完成后执行旧页面验收。资源管理是高风险基础设施动作,不能用模糊通配符“清理缓存”代替目标识别。

发布回滚也可能跨越依赖安全更新。如果旧页面引用的脚本有已知漏洞,不应为了可复现无限期恢复旧代码;需要在已知内容基础上构建安全修复版本,并说明这不是字节相同回滚。内容恢复与软件安全分别评估,资产保留不等于旧版本永远可以上线。历史文件访问政策同样需要最小化公开范围。

对于公开图片和字体,旧地址长期可用通常方便外部引用,但许可撤销或隐私风险会要求移除。此时清理服务器与 CDN 只是可控部分,已有下载无法收回。不可变缓存承诺应在资产发布前审查信息分类,不能将之后难以删除的资源用来承载秘密或未公开内容。

最后观察资源四百零四率与页面版本,判断清理是否过早。指标维度采用有限版本与资源类型,具体地址保留采样日志,避免每个哈希都制造无限指标序列。资源缺失恢复后确认缓存错误副本消失,才算结束。长缓存优化同时包含生命周期、诊断与兼容政策,不能只报告命中率变高。

为一个动态导入故障画出引用链

旧 HTML 引用甲版入口脚本,入口脚本在用户点击搜索后导入甲版搜索 chunk,chunk 再加载索引与样式。发布乙时如果只保留主脚本,旧入口仍能执行但交互会失败。资源清单应覆盖整个引用图,而不是只保留 HTML 直接链接。CSS 字体、wasm、worker 脚本和运行时索引也需要登记版本关系。

图中的每个地址有自己的更新语义。哈希脚本不可变,固定索引可能随文章发布更新,API 则按实时权限返回。旧脚本读取新索引时,要有格式兼容或明确版本地址,不能假设所有资源都能独立最新。把格式版本与内容版本分开,可以新增文章而不强迫旧客户端重新下载程序,也能在格式变化时建立真实迁移。

验收保存甲版页面,部署乙后才触发延迟功能,检查所有引用可获取且媒体类型正确。如果平台把缺失脚本回退成二百首页,网络状态成功却会产生解析错误。检查正文类型与预期内容身份,才能区分缺失、错误路由和真实脚本异常。日志同时保留页面版本与资源地址,但限制高基数字段进入指标。

删除决策依据受支持引用而非当前目录

资产清理先取得当前版本与仍受支持历史版本的清单,求出仍被引用的资源集合,再列出候选删除。相同哈希资产可能被多版共用,按文件创建时间删除会误伤当前页面。旧页面尚未下载的动态资源也要保留,不能只依据缓存里已有字节判断。最长合理客户端寿命与产品支持政策共同决定范围。

提供删除预览与可恢复归档,实际操作使用精确路径。清理完成后运行保存的旧页面测试,确认兼容承诺仍然成立。若决定停止支持某版,客户端应有可理解的更新路径,并保留未保存任务,不能通过故意删除 chunk 强迫所有人刷新。资源管理是支持政策的实施,不是把磁盘空间问题直接转嫁给用户。

安全修复可能要求不再提供旧脚本,但已有下载无法被服务器删除。敏感操作依赖当前服务端授权与协议兼容,不信任客户端自报版本。清理旧资产可以减少继续分发,不能作为阻止旧代码执行危险业务的唯一措施。内容哈希地址公开且可长期保存,因此秘密在发布前就必须被排除。

HTML 切换与资源上传的两阶段检查

先上传乙版不可变资源,再逐项验证,最后切换 HTML,可减少引用未就绪的窗口。检查实际域名下的地址,不仅对象存储后台,因为路径重写和 CDN 可能改变可达性。若新文件曾经被缓存成缺失,上传成功之后仍需要清理或等待明确错误有效期。正文切换前先解决这些已知失败,比上线后让读者遇到加载错误更稳。

这不是自动的全站事务。不同 HTML 可能在切换期间来自不同版本,浏览器也持有旧页面。应用设计需要接受受支持的版本组合,或采用确实支持版本路由的平台能力。报告具体平台保证,不用“原子部署”泛称遮盖缓存和客户端共存。回滚也检查资源集合,而不是只把首页指针改回去。

为缓存策略留出可解释的例外

一年 immutable 适用于真正不可变资源,固定 favicon、站点验证文件或某些下载地址可能采用不同更新方式。按用途分别配置,不为了规则统一给所有路径相同头。文件名有哈希但内容被手工覆盖,仍然违反承诺;文件名没有哈希但应用用可靠版本参数,也需要确认所有缓存层都把参数视为资源身份。

第三方固定 URL 不受本站控制,不能声明对方永久不变。可以使用供应商版本化地址并审查许可,或者接受外部更新风险并制定检查。重新托管前确认权利与维护责任,不能只为了长缓存下载所有外部资产。资源版本化是一项真实内容承诺,不是给任意文件名加几位字符。

清单还要覆盖预加载。HTML 预加载甲版字体,CSS 却引用乙版地址,浏览器可能下载两份资源而页面仍然正常。检查 preload、模块入口、CSS 和运行时导入是否来自同一批准引用图,不能只检查最终控件有没有显示。资源都二百也可能支付了无意义传输,这属于版本协调的性能失败。

对于持续打开页面的用户,资源支持期限可以通过有限版本统计与受控错误样本观察,而不是收集完整浏览历史。停止支持版本时给出更新条件,任务状态是否可保留由产品定义。浏览器已经缓存的旧脚本可能继续执行,所以服务端接口兼容期限也需要一起考虑,资产清理并不能单独决定旧程序何时退出。

自动恢复要避免扩大用户损失

chunk 失败后自动刷新一次仍可能丢失表单或重复副作用。先确定任务类型,搜索可以保留查询后重试,编辑器需要保留非敏感输入并说明更新需求。重复刷新设置上限,恢复失败后停止并提供稳定入口。网络离线、扩展阻断与版本缺失可能有不同恢复动作,不能所有错误都重载页面。

监控记录资源错误、当前页面版本与尝试次数,恢复完成后验证缺失缓存副本已经消失。高命中率不等于版本协调正确,低错误率也可能因为延迟功能很少被使用。跨版本任务完成是更直接证据,长缓存的收益必须与这种兼容承诺一起维护。

验收跨版本而不是只验新页面

保存上一版本 HTML,再部署新版本,用它请求旧资源,确认仍能工作。保持一个旧标签页,点击懒加载功能,观察是否出现缺失 chunk。请求新页面并检查所有资源返回内容与文件名一致。

还要验证回滚:旧 HTML 能否重新得到完整旧资源,缓存头有没有让错误覆盖长期残留。长缓存的收益建立在不可变资产与完整版本保留之上;缺少这两项,TTL 越长只是把更新故障延长。

短哈希名称还需要处理命名冲突。若自建发布器把完整摘要截成少量字符,两个不同资产有可能取得相同目标路径;不能因为发生概率看起来低就允许覆盖。上传遇到同名对象时比较已存在字节与待上传字节,相同可以复用,不同必须阻断或按照批准规则选择新地址。成熟构建器的命名机制优先于临时实现。

测试不必真的寻找加密摘要碰撞。给合成发布器提供可控命名替身,让两份不同文件产生同一路径,断言第二份不会覆盖第一份,且 HTML 尚未切换。这个样本只验证冲突处理路径,不证明摘要算法本身安全。不可变承诺依赖命名与写入两层共同维护,上传成功不应掩盖一个旧 URL 已改变内容的事实。

参考:Cache-Control、Vite 静态资产处理。