图像缓存策略完全指南 - Cache-Control、ETag 与 CDN 配置
Web 图像缓存基础 - 浏览器缓存与 CDN 缓存的角色
图像通常占网页总传输量的 50-70%。有效的缓存策略可以消除重复下载,显著提升页面加载速度和用户体验。
缓存层次:浏览器内存缓存(最快,页面会话内有效)→ 浏览器磁盘缓存(较快,跨会话持久化)→ CDN 边缘缓存(减少源站负载)→ 源站。每一层都可以独立配置缓存策略。
缓存的核心权衡:缓存时间越长,性能越好(减少请求),但内容更新的延迟也越大。图像的特点是内容一旦发布很少修改,因此适合长时间缓存。关键是设计一个在需要更新时能可靠失效的机制。
不可变资源策略:对于文件名包含内容哈希的图像(如 hero-a3f2b1.jpg),可以设置极长的缓存时间(1 年),因为内容变化时文件名也会变化,浏览器会请求新 URL。这是最高效的缓存模式。
再补充几点:具有代表性的 CDN 有 CloudFront、Cloudflare、Fastly 等。根据 HTTP Archive 的调查,只要缓存设置得当,图像请求的 60-80% 可以直接由缓存提供。设计缓存策略时,关键在于平衡「缓存的新鲜度(freshness)」与「缓存的失效(invalidation)」。
Cache-Control 头设计 - 指令组合
Cache-Control 是控制 HTTP 缓存行为的核心响应头。
常用指令:max-age=31536000(缓存 1 年)、no-cache(每次使用前必须验证)、no-store(完全不缓存)、public(CDN 可缓存)、private(仅浏览器可缓存)、immutable(内容永不变化,无需验证)。
推荐组合:
- 带哈希的静态图像:
Cache-Control: public, max-age=31536000, immutable - 可能更新的图像:
Cache-Control: public, max-age=86400, must-revalidate - 用户头像等动态图像:
Cache-Control: public, max-age=3600, must-revalidate - 敏感图像:
Cache-Control: private, no-store
immutable 指令告诉浏览器即使用户强制刷新也不需要重新验证,消除了不必要的条件请求。Chrome 和 Firefox 支持此指令。
stale-while-revalidate=86400 允许在后台重新验证期间继续使用过期缓存,用户不会感知到延迟。
不变的静态图像(带哈希的文件名)用 Cache-Control: public, max-age=31536000, immutable。文件名中包含内容哈希的图像(例如 hero-a1b2c3d4.jpg)在内容变化时文件名也会变化,因此可以安全地设置 1 年(31536000 秒)的缓存。immutable 指令表示浏览器连缓存的重新验证(conditional request)都不会执行。
ETag 与条件请求 - 高效的缓存重验证
当缓存过期时,条件请求允许服务器确认内容是否真的变化了,避免重新传输未修改的图像。
ETag 工作流程:服务器在响应中包含 ETag: "abc123"(通常是内容的哈希值)。浏览器缓存过期后,发送 If-None-Match: "abc123" 条件请求。如果内容未变,服务器返回 304 Not Modified(无响应体),浏览器继续使用缓存。
Last-Modified / If-Modified-Since:基于时间戳的替代方案。精度为秒级,不如 ETag 精确,但实现更简单。两者可以同时使用,ETag 优先级更高。
强 ETag vs 弱 ETag:强 ETag("abc123")表示字节级相同;弱 ETag(W/"abc123")表示语义等价(允许不重要的差异如空白变化)。图像通常使用强 ETag。
性能影响:304 响应虽然不传输图像数据,但仍有一次网络往返。对于带哈希文件名的图像,使用长 max-age + immutable 完全避免条件请求是更优方案。ETag 适合文件名不变但内容可能更新的场景。
ETag(Entity Tag)是标识资源某个特定版本的字符串。工作流程是:首次请求时服务器在响应中附加 ETag: "abc123";缓存过期后再次请求时浏览器发送 If-None-Match: "abc123" 头;内容没有变化则服务器返回 304 Not Modified(不含正文,仅数百字节),有变化则返回 200 OK 与新的图像数据。生成方式上,强 ETag 由文件的内容哈希(MD5、SHA-256 等)生成;而基于 inode 的 ETag(Apache 的默认设置)在每台服务器上都不同,因此应改为基于内容哈希的方式。
CDN 缓存设计 - CloudFront 与 Cloudflare 配置
CDN 在全球边缘节点缓存图像,减少源站负载并降低用户延迟。
CloudFront 配置:通过缓存策略(Cache Policy)控制 CDN 缓存行为。推荐为图像创建专用缓存策略:TTL 设为 86400-31536000 秒,启用 Gzip/Brotli 压缩(对 SVG 有效),根据 Accept 头进行内容协商(WebP/AVIF 自动选择)。
Cloudflare 配置:Page Rules 或 Cache Rules 设置图像路径的缓存级别。Browser Cache TTL 控制浏览器缓存时间。Polish 功能自动优化图像格式和质量。
缓存键设计:CDN 根据缓存键区分不同版本。默认缓存键是完整 URL。如果使用内容协商(同一 URL 返回不同格式),需要将 Accept 头加入缓存键,否则所有用户会收到第一个被缓存的格式。
源站保护:配置 CDN 的 Origin Shield(CloudFront)或 Tiered Caching(Cloudflare),减少回源请求。多个边缘节点的缓存未命中会合并为单次回源请求。
两大 CDN 的具体设置:CloudFront 的缓存策略可以使用托管策略 CachingOptimized,它依据 Cache-Control 与 Expires 头缓存,也能控制是否把查询字符串纳入缓存键;源请求策略则在需要把 Accept 头转发给源站、启用 WebP/AVIF 的 Content Negotiation 时设置。Cloudflare 的 Browser Cache TTL 在仪表盘中设置,选择「Respect Existing Headers」即直接沿用源站的 Cache-Control;Edge Cache TTL 用 Page Rules 按 URL 模式设置,常见做法是对 /images/* 设置为 1 个月;Polish 是图像自动优化功能,可以在 CDN 边缘执行 WebP 转换与无损压缩。缓存键除 URL 路径外还需包含用于格式协商的 Accept 头,这样同一个 URL 也能对支持 WebP 的浏览器返回 WebP、对不支持的浏览器返回 JPEG;实现方式是在 Vary 头中包含 Accept。
缓存失效策略 - 确保图像更新被及时反映
缓存失效是缓存系统中最困难的问题之一。需要在缓存效率和内容新鲜度之间找到平衡。
文件名哈希(推荐):在文件名中包含内容哈希(如 logo-v2f3a1b.png)。内容变化时哈希变化,URL 变化,浏览器自动请求新文件。无需主动失效缓存。构建工具(Webpack、Vite)自动处理。
CDN 缓存清除:通过 API 主动清除 CDN 上的缓存。CloudFront 使用 Invalidation(每月前 1000 次免费);Cloudflare 使用 Purge Cache。适合紧急更新但不应作为常规手段。
版本查询参数:image.jpg?v=2。简单但有缺陷 - 部分 CDN 默认忽略查询参数,需要配置将其纳入缓存键。不如文件名哈希可靠。
短 TTL + 条件请求:设置较短的 max-age(如 1 小时),过期后通过 ETag 验证。平衡了新鲜度和性能,但增加了条件请求的网络开销。
最佳实践:静态资源使用文件名哈希 + 长缓存;动态图像使用短 TTL + ETag;紧急更新使用 CDN Invalidation 作为补充手段。
失效策略有两种典型写法。策略 1 是文件名哈希:把图像命名为 hero-image-a1b2c3d4e5f6.jpg,图像更新时哈希改变、URL 也随之改变,旧缓存的问题被彻底解决;由于 HTML 侧的引用也要同时更新,前提是与构建流水线集成,用 Webpack 的 [contenthash] 或 Vite 的资源哈希功能即可自动化。策略 2 是用查询字符串做版本管理:写作 hero-image.jpg?v=20250723,不改文件名而用查询字符串管理版本。紧急更新时的手动清除,在 CloudFront 用 CreateInvalidation API、在 Cloudflare 用 Purge Cache API 执行。
Service Worker 高级缓存控制 - 离线支持
Service Worker 提供了完全可编程的缓存层,实现离线访问和精细的缓存策略。
缓存优先策略(Cache First):先检查缓存,命中则直接返回;未命中则请求网络并缓存结果。适合不常变化的图像资源。用户体验最快但可能显示过期内容。
网络优先策略(Network First):先请求网络,成功则更新缓存并返回;失败则回退到缓存。适合需要新鲜度但也需要离线支持的场景。
Stale While Revalidate:立即返回缓存内容(即使过期),同时在后台请求网络更新缓存。用户看到即时响应,下次访问看到更新内容。最佳的用户体验和新鲜度平衡。
图像专用缓存:创建独立的 Cache Storage 存储图像,设置大小上限(如 50MB)。超出限制时使用 LRU(最近最少使用)策略淘汰旧图像。
预缓存关键图像:在 Service Worker 安装阶段预缓存 Logo、图标等关键图像,确保离线时核心 UI 完整显示。使用 Workbox 库简化 Service Worker 的缓存管理。
Cache First 策略(最适合图像)的具体写法是 self.addEventListener("fetch", (event) => { if (event.request.destination === "image") { event.respondWith(caches.match(event.request).then((cached) => { return cached || fetch(event.request).then(async (response) => { const cache = await caches.open("images-v1"); cache.put(event.request, response.clone()); return response; }); })); } });,缓存命中时立即返回,只有未命中时才发起网络请求。容量管理方面,Service Worker 的缓存存储有上限(因浏览器而异,通常为 50MB-数百 MB),应实现用 LRU(Least Recently Used)自动删除旧条目的逻辑;使用 Workbox 库的 ExpirationPlugin 可以轻松设置最大条目数与最长保存期限。响应式图像方面,用 srcset 提供多种尺寸时,在 Service Worker 中只缓存与设备屏幕宽度相符的最佳尺寸,即可减少存储占用。