HTTP 缓存分为强缓存和协商缓存。
强缓存依据响应头的 max-age 和 expires 判断有效期,有效期内不发出网络请求,直接200。
协商缓存依据请求头的 if-none-match 和 if-modified-since 发出条件请求,校验通过返回304,失败则返回200更新本地缓存。而这两个首部是取自最近一次相同地址的响应头中的 etag 和 last-modified。
实际请求中会先尝试强缓存再走协商缓存。
当有人问我,什么是 HTTP 缓存? 我可能会脱口而出***** (像上面这样)。那真的是这样吗 🤔
HTTP 缓存的本质
『HTTP』是指 HTTP 协议。那么『HTTP 缓存』就是 HTTP 协议关于缓存的规范。当我们试图缓存响应的时候可以参考规范中的范式实现。
通俗点说,比如我一个中文初学者,想要用中文『打招呼』。手里刚好有一本这样的参考书。
打招呼常用语:
- 吃了没
- 今天天气挺好的
- 您的气色真不错
我选择其中一句问候我的朋友。如果朋友的中文不错,他很自然明白了我的意思。而如果朋友也是一个中文初学者,很可能他不明白我在说什么。而如果我们是关系很好的朋友,一句『嘿』就可以完成打招呼。
使用 HTTP 通信的场景不都有 HTTP 缓存,这取决于通信双方对规范的实现情况。 Postman 可以用来发送 HTTP 请求,但是由于其工具性质不实现 HTTP 缓存。而浏览器对 HTTP 缓存的实现比较全面。后文将探究浏览器实现的 HTTP 缓存。
缓存流程
一个 <img src="my-image.png">
产生的网络请求经过各级缓存的顺序也是『洋葱模型』。
- 最先经过 service worker 查找缓存。
- 接着就是浏览器缓存,强缓存、弱缓存都是这里。
- 然后服务端如果使用 CDN 网络,会经过各级 CDN 节点查找缓存。
- 最后到达后端服务。
浏览器缓存策略
可配置缓存策略
大部分前端页面产生的 HTTP 访问前端工程师无法控制其应用的缓存策略。有个例外,fetch API 的 cache。
// 跳过缓存
fetch('/a.js', {cache: 'no-store'}).then(res => {
});
名称 | 策略 | 请求头表现 |
---|---|---|
default |
依次尝试强缓存、协商缓存,获取响应后更新缓存 | 省略 cache-control pragma ,携带if-*首部 |
no-store |
跳过缓存,直接请求服务端,响应不用于更新缓存 | 省略两个if-* 首部 |
reload |
跳过缓存,直接请求服务端,获取响应后更新缓存 | 省略两个if-* 首部 |
no-cache |
请求服务端校验协商缓存,获取响应后更新缓存 | cache-control: max-age=0 ,携带if-*首部 |
force-cache |
有缓存用缓存,没缓存请求服务更新缓存(无论是否过时) | 省略 cache-control pragma ,携带if-*首部 |
only-if-cached |
有缓存用缓存,没有就报错 | 不会产生请求 |
此处
no-cache
不能和cache-control: no-cache
混淆。
no-store
和 reload
在一些浏览器中请求头会带有 cache-control: no-cache
。根据 HTTP 缓存关于请求首部规范,cache-control: no-cache
意为 『客户端希望在服务端验证有效的前提下应用缓存』;而 cache-control: max-age=0
意为『客户端应用有效期为0的强缓存,即跳过强缓存』。两者意思相近,而决定no-store
和 reload
跳过缓存的是省略两个if-* 首部。
默认缓存策略
1. 常规页面访问
场景:文档跳转、地址栏回车、点击刷新按钮、页面右键重新加载、command+R
- 根文档(html):应用类似 fetch cache no-cache 策略
- 其他资源(css、js、png…)应用类似 fetch cache default 策略
2. 硬性重新加载
场景:command+shift+R
- 根文档(html):应用类似 fetch cache no-cache 策略
- 其他资源(css、js、png…)初次请求应用类似 fetch cache no-cache 策略,重复请求应用default 策略
强缓存
强缓存在 Network 中表现为状态码200且标记为已缓存,直接应用客户端缓存,不发出 HTTP 请求。
满足以下三个条件可应用强缓存
- 缓存中的响应头不阻止强缓存:像
cache-control: no-cache
和pragma: no-cache
就不行。 - 当前策略不阻止强缓存:像
cache: no-cache
和硬性重新加载就不行。 - 缓存在有效期内:明确的时间或启发式的推断。
明确时间的强缓存
1. max-age
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Cache-Control: max-age=5
<!doctype html>
…
HTTP1.1 规范中的首部 cache-control 取值 max-age=[namber] ,表示当前响应在缓存中有效的时间段。
2. Expires
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Expires: Tue, 22 Feb 2022 22:22:22 GMT
<!doctype html>
…
Expires 是 HTTP1.0 规范的首部,表示当前响应在缓存中过时的时间点。
max-age 的优先级高于 Expires 。
启发式缓存
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2021 22:22:22 GMT
<!doctype html>
…
在缓存的响应没有 Cache-Control
、Pragma
和 Expires
的时候,浏览器将依据 Last-Modified
和 Date
计算一个推断的有效期。
n 是一个0到1的系数,规范建议取0.1。以前面的例子来说,这条缓存距离服务端资源变更已有1年,因此推断它还能使用0.1年,在'Tue, 22 Feb 2022 22:22:22 GMT'+0.1年
后过时。
协商缓存
协商缓存将发出带有 if-none-match
或 if-modified-since
的 HTTP 条件请求,服务端校验缓存是否有效。
- 如果校验通过,则复用本地缓存,状态码304。
- 如果校验失败,则返回新的响应,状态码200。
依据响应头中的cache-control取值是否更新缓存。
请求头中有两个 if-* 之一是协商缓存的必要条件。由于缓存中没有对应的特征首部(last-modified和etag)或者前文缓存策略导致省略两个 if-*都不能完成协商缓存。
if-none-match
GET /a HTTP/1.1
Host: localhost:3000
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
If-None-Match: 123
if-none-match 在 HTTP1.1 规范中意为 『如果没有命中该值则需要服务端处理』 。抛开缓存策略的影响,if-none-match 通常取自缓存响应头的 etag ,是响应的特征串(响应体长度、hash或者叠加运算)。
if-modified-since
GET /a HTTP/1.1
Host: localhost:3000
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
If-Modified-Since: Tue, 22 Feb 2022 22:22:22 GMT
if-modified-since 在 HTTP1.1 规范中意为 『如果在改时间点后发生了变更则需要服务端处理』 。抛开缓存策略的影响,if-modified-since 通常取自缓存响应头的 last-modified ,是响应资源的最近修改时间。
浏览器缓存流程
这里省略了强缓存和协商缓存的细节,不过经过上面的解释还有您的长期积累相信可以补充出来。
以上也只是本人看规范、写 demo、对比几家之言,得到的一点总结,欢迎讨论指正。
延伸阅读
原文链接:https://juejin.cn/post/7236670795061329977 作者:蓝色夜晚