快速上手 HTTP Cache

2025-01-13 pv

开始系统研究 HTTP Cache,是因为我在开发 裁员追踪器 | Layoffs Tracker🔗 遇到的一个问题:

API 返回的接口数据是静态的,很长时间都不会更新,如果每次都需要访问服务器,一来效率低下,二来会对服务器造成不必要的压力。

所以考虑加一层 Cache。

最简单的实现方式,就是 HTTP Cache。

本来以为,只是加几个 HTTP Header 的事情,后来研究一番后发现,这一块的内容,远比想象中丰富。

所以萌发出,写一篇文章,体系化梳理 HTTP Cache,重点将放在:产生的背景,解决的手段,以及最佳实践。

1. 背景

Cache(缓存)在整个计算机系统里被广泛使用。

从最底层的 CPU Cache,到 DNS 解析,CDN,再到耳熟能详的 Redis,无一不是这个概念的应用。

用少量空间,将耗时的计算提前预存,使用的时候直接获取。

内在原理,是局部性(Locality),这其中又分为时间局部性和空间局部性

意思是,短期内访问的内存,在未来更有可能被访问;附近的内存,同样存在被更大概率访问的可能。

HTTP Cache 也是如此。

不仅可以加快网页的加载速度,优化用户体验,同时帮助减轻服务器负载。

举个例子。

如果一个静态文件,长期不被修改,那么每次刷新网页时都重新请求是不必要的。一来速度慢,二来增加网络访问开销,如果文件尺寸较大,又会给服务器带来一定压力。

HTTP Cache 可以很好地解决这个问题。第一次访问时,缓存在本地,第二次访问便可避开网络请求,直接从本地获取。

2. 解决方案

HTTP Cache 主要有两种:

  • 强制缓存,客户端直接使用本地缓存
  • 协商缓存,客户端向服务器发送请求,由服务器决定是否使用缓存

2.1 强制缓存

强制缓存的关键 header 有两个:

  • Expires(不推荐),指定过期的时间点,是一个绝对时间,例如:Expires: Wed, 15 Jan 2025 20:00:00 GMT
  • Cache-Control(推荐),一种更加精细,且更通用的控制手段。常用指令:
    • public: 表示资源可以被任何缓存共享,例如代理服务器
    • private: 表示资源仅限于用户终端缓存(私有),代理服务器不可以
    • no-cache: 每次请求必须向服务器请求验证
    • no-store: 表示完全不允许缓存
    • max-age=3600: 表示资源有效期为 3600 秒
    • s-maxage=3600:覆盖 max-ageExpires,但仅用于共享缓存,私有缓存会忽略

我在 裁员追踪器 | Layoffs Tracker🔗 用到的配置如下:

缓存的时间为 60 秒,且可以被共享。

2.2 协商缓存

相比强制缓存这种单边决定,协商缓存更加复杂:需要客户端和服务端的双向配合

关键头部有两部分,请求头和响应头。

  • 请求头:
    • If-Modified-Since: 上次修改时间,用于配合 Last-Modified
    • If-None-Match: 本地的缓存版本标识(ETag)
  • 响应头:
    • Last-Modified,最后一次修改的时间,可用于快速比较资源是否发生变化,但精度不如 ETag
    • ETag,资源的唯一标识,如果原始资源变更,ETag 必须重新生成,通过比较 ETag 可以高效、准确地判断是否有变化发生

具体的工作流程是:

  1. 客户端在第一次请求资源时,会收到 Last-ModifiedETag 信息
  2. 客户端再次请求资源,附带 If-Modified-SinceIf-None-Match字段
  3. 服务端根据请求字段,判断缓存是否有效,
    • 如果有效,返回 304 Not Modified,客户端直接使用缓存
    • 如果无效,返回新的资源及其状态码 200 OK

类似上面的例子,访问某个 .js 资源:

2.3 优先级

强制缓存和协商缓存可以同时存在。

同时存在时,会优先判断强制缓存是否有效,无效则进一步使用协商缓存。

当然,如果 Cache-Control: no-store 存在,缓存功能被禁用。

3. 最佳实践

不同的场景适合不同的缓存策略。

对于静态文件,如 CSS、JS 和图像等,可以通过设置较长的缓存时间,同时将版本信息填充到文件名里,避免因缓存时间过久导致未能及时获取最新数据。例如:

  • Cache-Control: max-age=31536000
  • 文件名示例:style.v2.cssapp.123abc.js

对于动态资源而言,变动频率较高,可以利用协商缓存:

  • ETagLast-Modified 提供内容变更验证机制
  • 响应头设置 Cache-Control: no-cache

对于涉及登陆等用户隐私相关的,建议禁止缓存:

  • 响应头设置 Cache-Control: no-store, private

4. 总结

在优化性能方面,缓存是个简单而强大的工具。

以空间换时间。在准确性和效率中间寻找折衷。

组合使用 HTTP Cache 提供的功能,优化加载和网络访问,差不多是前端的必修课。

毕竟,在复杂多变的网络环境中,缓存多少提供了一些确定性。

这个世界很公平。任何选择,都会带来一些结果,以及代价。

(完)

参考

  1. HTTP 缓存 - HTTP | MDN🔗
  2. Prevent unnecessary network requests with the HTTP Cache | Articles | web.dev🔗
  3. Cache-Control - HTTP | MDN🔗
在 GitHub 上编辑本页面

最后更新于: 2025-01-15T05:49:17+08:00