写在前面的话,我们通常意义上认为的标准答案是什么?
教科书、知名博客、官方约定、实践 ......?但是,就语言、行为的标准答案是什么呢?那就是 规范
,这里的规范有两个含义:
规范定义
某个组织(
w3c
)......
规范实现
浏览器厂商(
chrome
、edge
......)服务器供应商 (
apache
......)
引子
提及浏览器缓存,你的脑海里能回想起哪些缓存类型呢?
数据缓存
localStorage
sessionStorage
IndexedDB
Web SQL Database
离线缓存
Service Worker
http
缓存强缓存
协商缓存
其实 http
缓存的分类,严格意义上的区分归类是:私有缓存 和 共享缓存
也即:cache-control: private or
cache-control: public
那么默认行为是什么呢? https://datatracker.ietf.org/doc/html/rfc7234#section-4.2.2
A cache MUST NOT use heuristics to determine freshness when an explicit expiration time is present in the stored response. Because of the requirements in Section 3, this means that, effectively, heuristics can only be used on responses without explicit freshness whose status codes are defined as cacheable by default (see Section 6.1 of [RFC7231]), and those responses without explicit freshness that have been marked as explicitly cacheable (e.g., with a "public" response directive).
只要明确是 启发式 缓存的请求,都会被赋予默认 public
的行为。这里的阐述稍微有一些模糊,我们可以溯源至上一个版本的规范
The max-age directive on a response implies that the response is cacheable (i.e.,"public") unless some other, more restrictive cachedirective is also present.
当设置了 max-age
时,即为 public
,其实,启发式缓存中的通用方案也是 cache-control
,具体的阐释,我们可以看 MDN
Heuristic caching is a workaround that came before
Cache-Control
support became widely adopted, and basically all responses should explicitly specify aCache-Control
header.
但是,今天的重点是 http 缓存,其余的内容不做过多的阐释。
新鲜度
缓存中有个很重要的概念,那就是新鲜度的计算,因为只有客户端或者中继服务认为缓存新鲜,才会继续重用缓存。
A fresh response is one whose age has not yet exceeded its freshness lifetime. Conversely, a stale response is one where it has.
新鲜响应是指其年龄尚未超过其新鲜期的响应。反之,陈旧的响应是指它已经超过了。
A response's freshness lifetime is the length of time between its generation by the origin server and its expiration time. An explicit expiration time is the time at which the origin server intends that a stored response can no longer be used by a cache without further validation, whereas a heuristic expiration time is assigned by a cache when no explicit expiration time is available.
响应的保鲜期是指从源服务器生成到过期时间之间的时间长度。明确的过期时间是指源服务器打算让缓存在没有进一步验证的情况下不再使用存储的响应的时间,而启发式过期时间是由缓存在没有明确过期时间的情况下指定的。
确定新鲜度的主要机制是源服务器使用Expires标头字段(第 5.3 节)或 max-age 响应指令(第 5.2.2.8 节)提供未来的明确到期时间。
公式:response_is_fresh = (freshness_lifetime > current_age)
freshness_lifetime
如果缓存是共享的并且存在 s-maxage 响应指令(第 5.2.2.9 节),则使用它的值.
如果存在 max-age 响应指令(第 5.2.2.8 节),请使用它的值.
否则,响应中不存在明确的到期时间。启发式的新鲜度生命周期可能适用;请参阅第 4.2.2 节。
current_age
- 由于规范内容过多,并非本文的重点,这里不再做赘述,详细可查看:传送门
总结一句话:通过 Cache-Control
和 Expires
,原始服务器可以对资源定义其保质期。在保质期之内,缓存就认为该资源是新鲜的,可以直接传回给客户端,如果过期那么就需要进行再验证。
当然,再次之前我们需要明确一个点:强缓存和协商缓存的优先级如何?
实际上,在 Http
规范中有作描述:当已经明确了过期时间,那么将不会采用启发式缓存。
A cache MUST NOT use heuristics to determine freshness when an explicit expiration time is present in the stored response.
所以结论:强缓存 >
协商缓存
Http 1.0 时代
强缓存:
- Expires 需要使用固定格式的绝对日期:https://www.rfc-editor.org/rfc/rfc822#section-5.1
注意:与 Expires 做对比的 Date
均来自服务器。
这里我们简单示例下,当 Expires
小于 Date
时,缓存将不会被重用。
可以看出,当第 31
秒的时候,相对于服务器时间的 30
秒,过期后,即从服务器重新获取资源,而不是从客户端的 disk
或者 memory
中获取资源。
协商缓存
Request Header:
If-Modified-Since
Response Header:
Last-Modified
当 If-Modified-Since
陈旧时,将会重新向服务器请求资源,否则 304
。
Http 1.1 时代
强缓存:
- cache-control: max-age=xxx
可以看到,当设置了 cache-control: max-age: 30
30默认是秒级别,浏览器会缓存内容,过期后,继续从服务器获取。
协商缓存:
Request Header:
If-None-Match
Response Header:
Etag
可以看到,当服务器默认开了 etag
时,客户端请求时,会携带 If-None-Match
进行比对协商。
好的,到这里,基本的缓存知识已经讲解完毕了,接下来我们详细介绍下在不同缓存组合下,浏览器或者服务器是如何处理的,我们将围绕以下几个问题开展:
Expires
和Cache-Control
同时存在,浏览器作何选择?Http 1.0
和Http 1.1
中的协商头都存在,服务器作何选择?
Expires和
Cache-Control` 同时存在,浏览器作何选择?
在http规范中是这样描述的:
If a response includes a Cache-Control field with the max-age directive (Section 5.2.2.8), a recipient MUST ignore the Expires field. Likewise, if a response includes the s-maxage directive (Section 5.2.2.9), a shared cache recipient MUST ignore the Expires field. In both these cases, the value in Expires is only intended for recipients that have not yet implemented the Cache-Control field.
简单理解:如果两个头都存在,那么会忽略 Expires
。
Http 1.0
和 Http 1.1
中的协商头都存在,服务器作何选择?
这个问题的答案在 MDN
中是这么描述的:
Note: When evaluating how to use
ETag
andLast-Modified
, consider the following: During cache revalidation, if bothETag
andLast-Modified
are present,ETag
takes precedence. Therefore, if you are only considering caching, you may think thatLast-Modified
is unnecessary. However,Last-Modified
is not just useful for caching; instead, it is a standard HTTP header that is also used by content-management (CMS) systems to display the last-modified time, by crawlers to adjust crawl frequency, and for other various purposes. So considering the overall HTTP ecosystem, it is preferable to provide bothETag
andLast-Modified
.
以 Etag
优先,但是思考一下,这个表述严谨吗?
我在 MDN
的 issue
里发表了我的观点,更严谨的说法应该是:规范建议如此,但是具体的优先级还是看缓存服务器的实现细节。
最终和几个维护者达成了一致的结论:MDN
的内容应该是基于规范 (有时提到浏览器实现)。
但是最终还是做了更正,因为大家应该始终达成一个共识就是 协商缓存的校验行为始终发生在服务器端
真正做到有据可循,概念上至少不会让读者模棱两可,产生分歧或者误解。
HTTP 1.1 规则 13.3.4
HTTP/1.1 clients:
- If an entity tag has been provided by the origin server, MUST use that entity tag in any cache-conditional request (using If- Match or If-None-Match).
- If only a Last-Modified value has been provided by the origin server, SHOULD use that value in non-subrange cache-conditional requests (using If-Modified-Since).
如何验证我的观点呢?
Express
服务器新鲜度当校验
当然这里是一个 bug
,详见issue
地址,但是作者迟迟没有修复,不管是否 bug
与否,这里其实更想表述的是,具体的实现取决于服务器的实现。
我们实际操作一下:当同时具备两个头时,变更请求的 If-Modify-Since
可以看到,服务器同样做了响应处理。
题外探索
Etag
的生成策略如何?
在介绍以下内容时,我希望你可以忘记过去的一些不完全正确的知识结论:Etag 根据 INode 生成、分布式系统下会出现缓存失效的场景。
Apache
默认情况下,
Apache 2.3.14
和更早的版本在ETag
中包含了INode
-- 这意味着相同的资产的ETag
在不同的服务器之间会发生变化!这不再是默认情况,你可以在Apache
中配置产生ETag
的方法。默认情况下,这不再包括在内,你可以配置Apache
中用来产生ETag
的东西。默认是使用最后修改时间和文件大小,但你也可以选择使用文件摘要,并手动将INode
纳入计算。
Nginx 源码一览
Express
最佳的缓存形式如何?
那么对于我们生产实践的项目而言,最佳的缓存策略如何呢?
这里列举一下最佳实践:
Use
Cache-control
HTTP directive to control who can cache the response, under which conditions, and for how long.Configure your server or application to send validation token Etag.
Do not cache HTML in the browser. Always set
cache-control: no-store, no-cache
before sending HTML response to the client-side.Embed fingerprints in the URL of static resources like image, JS, CSS, and font files.
Safely cache static resources, i.e., images, JS, CSS, font files for a longer duration like six months.
Avoid embedding timestamps in URLs as this could quickly increase the variations of the same content on a local cache (and CDN), which will ultimately result in a lower cache hit ratio.
Use Vary header responsibly. Avoid using
Vary: User-agent
in the response header.Consider implementing CDN purge API in your CMS if you need to purge content from the cache in an automated way.
CDN
的原理
Req
& Res
同时设置 max-age
的优先级怎么处理?
暂时探索下来依旧以 Res
优先
朋友劝我不要钻牛角尖,但是我在 stack overflow
看到了跟我一样钻牛角尖的人后,这样热情不减反增。
小结
本篇文章总结到这里就算结束了,对你的耐心阅读点个赞 👍,相信对于缓存知识你也有了一定的认识和理解,同时对于知识的对与错也有了一定的探究方法论。
诸如 Etag 优先论
、先校验
Etag再校验
Last-Modified-Time论
、分布式缓存服务器失效论
......等千篇一律的论调,相信你也有自己的判定以及斟酌。
总之,求知求真的过程是令人很享受的,如果你有兴趣,不妨跟着本文一起探索探索,从读规范开始,再到实践中去,凡事问个 为什么,追求真解的过程中保持理智,虚心求教。