深入浅出浏览器缓存

·

5 min read

写在前面的话,我们通常意义上认为的标准答案是什么?

教科书知名博客官方约定实践 ......?但是,就语言、行为的标准答案是什么呢?那就是 规范,这里的规范有两个含义:

  • 规范定义

    • 某个组织(w3c

    • ......

  • 规范实现

    • 浏览器厂商(chromeedge ......)

    • 服务器供应商 (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 a Cache-Controlheader.

但是,今天的重点是 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 节),请使用它的值.

    • 如果存在Expires响应头字段(第 5.3 节),则使用其值减去Date响应头字段的值.

    • 否则,响应中不存在明确的到期时间。启发式的新鲜度生命周期可能适用;请参阅第 4.2.2 节

  • current_age

    • 由于规范内容过多,并非本文的重点,这里不再做赘述,详细可查看:传送门

总结一句话:通过 Cache-ControlExpires ,原始服务器可以对资源定义其保质期。在保质期之内,缓存就认为该资源是新鲜的,可以直接传回给客户端,如果过期那么就需要进行再验证。


当然,再次之前我们需要明确一个点:强缓存和协商缓存的优先级如何?

实际上,在 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 做对比的 Date 均来自服务器。

这里我们简单示例下,当 Expires 小于 Date 时,缓存将不会被重用。

可以看出,当第 31 秒的时候,相对于服务器时间的 30 秒,过期后,即从服务器重新获取资源,而不是从客户端的 disk 或者 memory 中获取资源。

协商缓存

  • Request HeaderIf-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 HeaderIf-None-Match

  • Response Header: Etag

可以看到,当服务器默认开了 etag 时,客户端请求时,会携带 If-None-Match 进行比对协商。


好的,到这里,基本的缓存知识已经讲解完毕了,接下来我们详细介绍下在不同缓存组合下,浏览器或者服务器是如何处理的,我们将围绕以下几个问题开展:

  • ExpiresCache-Control 同时存在,浏览器作何选择?

  • Http 1.0Http 1.1 中的协商头都存在,服务器作何选择?

ExpiresCache-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.0Http 1.1 中的协商头都存在,服务器作何选择?

这个问题的答案在 MDN 中是这么描述的:

Note: When evaluating how to use ETag and Last-Modified, consider the following: During cache revalidation, if both ETag and Last-Modified are present, ETag takes precedence. Therefore, if you are only considering caching, you may think that Last-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 both ETag and Last-Modified.

Etag 优先,但是思考一下,这个表述严谨吗?

我在 MDNissue 里发表了我的观点更严谨的说法应该是:规范建议如此,但是具体的优先级还是看缓存服务器的实现细节。

最终和几个维护者达成了一致的结论: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

源码一览

最佳的缓存形式如何?

那么对于我们生产实践的项目而言,最佳的缓存策略如何呢?

这里列举一下最佳实践

  1. Use Cache-control HTTP directive to control who can cache the response, under which conditions, and for how long.

  2. Configure your server or application to send validation token Etag.

  3. Do not cache HTML in the browser. Always set cache-control: no-store, no-cache before sending HTML response to the client-side.

  4. Embed fingerprints in the URL of static resources like image, JS, CSS, and font files.

  5. Safely cache static resources, i.e., images, JS, CSS, font files for a longer duration like six months.

  6. 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.

  7. Use Vary header responsibly. Avoid using Vary: User-agent in the response header.

  8. 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分布式缓存服务器失效论......等千篇一律的论调,相信你也有自己的判定以及斟酌。

总之,求知求真的过程是令人很享受的,如果你有兴趣,不妨跟着本文一起探索探索,从读规范开始,再到实践中去,凡事问个 为什么,追求真解的过程中保持理智,虚心求教。