Http 缓存剖析
缓存一直是前端优化的主战场, 利用好缓存就成功了一半. 本篇从http请求和响应的头域入手, 让你对浏览器缓存有个整体的概念. 最终你会发现强缓存, 协商缓存 和 启发式缓存是如此的简单.
导读
浏览器对于请求资源, 拥有一系列成熟的缓存策略. 按照发生的时间顺序分别为存储策略, 过期策略, 协商策略, 其中存储策略在收到响应后应用, 过期策略, 协商策略在发送请求前应用. 流程图如下所示.
废话不多说, 我们先来看两张表格.
1.http header中与缓存有关的key.
key | 描述 | 存储策略 | 过期策略 | 协商策略 |
---|---|---|---|---|
Cache-Control | 指定缓存机制,覆盖其它设置 | ✔️ | ✔️ | |
Pragma | http1.0字段,指定缓存机制 | ✔️ | ||
Expires | http1.0字段,指定缓存的过期时间 | ✔️ | ||
Last-Modified | 资源最后一次的修改时间 | ✔️ | ||
ETag | 唯一标识请求资源的字符串 | ✔️ |
2.缓存协商策略用于重新验证缓存资源是否有效, 有关的key如下.
key | 描述 |
---|---|
If-Modified-Since | 缓存校验字段, 值为资源最后一次的修改时间, 即上次收到的Last-Modified值 |
If-Unmodified-Since | 同上, 处理方式与之相反 |
If-Match | 缓存校验字段, 值为唯一标识请求资源的字符串, 即上次收到的ETag值 |
If-None-Match | 同上, 处理方式与之相反 |
下面我们来看下各个头域(key)的作用.
Cache-Control
浏览器缓存里, Cache-Control是金字塔顶尖的规则, 它藐视一切其他设置, 只要其他设置与其抵触, 一律覆盖之.
不仅如此, 它还是一个复合规则, 包含多种值, 横跨 存储策略, 过期策略 两种, 同时在请求头和响应头都可设置.
语法为: “Cache-Control : cache-directive”.
Cache-directive共有如下12种(其中请求中指令7种, 响应中指令9种):
Cache-directive | 描述 | 存储策略 | 过期策略 | 请求字段 | 响应字段 |
---|---|---|---|---|---|
public | 资源将被客户端和代理服务器缓存 | ✔️ | ✔️ | ||
private | 资源仅被客户端缓存, 代理服务器不缓存 | ✔️ | ✔️ | ||
no-store | 请求和响应都不缓存 | ✔️ | ✔️ | ✔️ | |
no-cache | 相当于max-age:0,must-revalidate 即资源被缓存, 但是缓存立刻过期, 同时下次访问时强制验证资源有效性 |
✔️ | ✔️ | ✔️ | ✔️ |
max-age | 缓存资源, 但是在指定时间(单位为秒)后缓存过期 | ✔️ | ✔️ | ✔️ | ✔️ |
s-maxage | 同上, 依赖public设置, 覆盖max-age, 且只在代理服务器上有效. | ✔️ | ✔️ | ✔️ | |
max-stale | 指定时间内, 即使缓存过时, 资源依然有效 | ✔️ | ✔️ | ||
min-fresh | 缓存的资源至少要保持指定时间的新鲜期 | ✔️ | ✔️ | ||
must-revalidation / proxy-revalidation | 如果缓存失效, 强制重新向服务器(或代理)发起验证(因为max-stale等字段可能改变缓存的失效时间) | ✔️ | ✔️ | ||
only-if-cached | 仅仅返回已经缓存的资源, 不访问网络, 若无缓存则返回504 | ✔️ | |||
no-transform | 强制要求代理服务器不要对资源进行转换, 禁止代理服务器对 Content-Encoding , Content-Range , Content-Type 字段的修改(因此代理的gzip压缩将不被允许) |
✔️ | ✔️ |
假设所请求资源于4月5日缓存, 且在4月12日过期.
当max-age 与 max-stale 和 min-fresh 同时使用时, 它们的设置相互之间独立生效, 最为保守的缓存策略总是有效. 这意味着, 如果max-age=10 days, max-stale=2 days, min-fresh=3 days, 那么:
- 根据max-age的设置, 覆盖原缓存周期, 缓存资源将在4月15日失效(5+10=15);
- 根据max-stale的设置, 缓存过期后两天依然有效, 此时响应将返回110(Response is stale)状态码, 缓存资源将在4月14日失效(12+2=14);
- 根据min-fresh的设置, 至少要留有3天的新鲜期, 缓存资源将在4月9日失效(12-3=9);
由于客户端总是采用最保守的缓存策略, 因此, 4月9日后, 对于该资源的请求将重新向服务器发起验证.
Pragma
http1.0字段, 通常设置为Pragma:no-cache
, 作用同Cache-Control:no-cache
. 当一个no-cache请求发送给一个不遵循HTTP/1.1的服务器时, 客户端应该包含pragma指令. 为此, 勾选☑️ 上disable cache时, 浏览器自动带上了pragma字段. 如下:
Expires
Expires:Wed, 05 Apr 2017 00:55:35 GMT
即到期时间, 以服务器时间为参考系, 其优先级比 Cache-Control:max-age
低, 两者同时出现在响应头时, Expires
将被后者覆盖. 如果Expires
, Cache-Control: max-age
, 或 Cache-Control:s-maxage
都没有在响应头中出现, 并且也没有其它缓存的设置, 那么浏览器默认会采用一个启发式的算法, 通常会取响应头的Date_value - Last-Modified_value
值的10%作为缓存时间.
如下资源便采取了启发式缓存算法.
其缓存时间为 (Date_value - Last-Modified_value) * 10%
, 计算如下:
const Date_value = new Date('Thu, 06 Apr 2017 01:30:56 GMT').getTime();
const LastModified_value = new Date('Thu, 01 Dec 2016 06:23:23 GMT').getTime();
const cacheTime = (Date_value - LastModified_value) / 10;
const Expires_timestamp = Date_value + cacheTime;
const Expires_value = new Date(Expires_timestamp);
console.log('Expires:', Expires_value); // Expires: Tue Apr 18 2017 23:25:41 GMT+0800 (CST)
可见该资源将于2017年4月18日23点25分41秒过期, 尝试以下两步进行验证:
1) 试着把本地时间修改为2017年4月18日23点25分40秒, 迅速刷新页面, 发现强缓存依然有效(依旧是200 OK (from disk cache)
).
2) 然后又修改本地时间为2017年4月18日23点26分40秒(即往后拨1分钟), 刷新页面, 发现缓存已过期, 此时浏览器重新向服务器发起了验证, 且命中了304协商缓存, 如下所示.
3) 将本地时间恢复正常(即 2017-04-06 09:54:19). 刷新页面, 发现Date依然是4月18日, 如下所示.
从⚠️ Provisional headers are shown
和Date字段可以看出来, 浏览器并未发出请求, 缓存依然有效, 只不过此时Status Code显示为200 OK. (甚至我还专门打开了charles, 也没有发现该资源的任何请求, 可见这个200 OK多少有些误导人的意味)
可见, 启发式缓存算法采用的缓存时间可长可短, 因此对于常规资源, 建议明确设置缓存时间(如指定max-age 或 expires).
ETag
ETag:"fcb82312d92970bdf0d18a4eca08ebc7efede4fe"
实体标签, 服务器资源的唯一标识符, 浏览器可以根据ETag值缓存数据, 节省带宽. 如果资源已经改变, etag可以帮助防止同步更新资源的相互覆盖. ETag 优先级比 Last-Modified 高.
If-Match
语法: If-Match: ETag_value 或者 If-Match: ETag_value, ETag_value, …
缓存校验字段, 其值为上次收到的一个或多个etag
值. 常用于判断条件是否满足, 如下两种场景:
- 对于 GET 或 HEAD 请求, 结合 Range 头字段, 它可以保证新范围的请求和前一个来自相同的源, 如果不匹配, 服务器将返回一个416(Range Not Satisfiable)状态码的响应.
- 对于 PUT 或者其他不安全的请求,
If-Match
可用于阻止错误的更新操作, 如果不匹配, 服务器将返回一个412(Precondition Failed)状态码的响应.
If-None-Match
语法: If-None-Match: ETag_value 或者 If-None-Match: ETag_value, ETag_value, …
缓存校验字段, 结合ETag字段, 常用于判断缓存资源是否有效, 优先级比If-Modified-Since
高.
- 对于 GET 或 HEAD 请求, 如果其etags列表均不匹配, 服务器将返回200状态码的响应, 反之, 将返回304(Not Modified)状态码的响应. 无论是200还是304响应, 都至少返回
Cache-Control
,Content-Location
,Date
,ETag
,Expires
, andVary
中之一的字段. - 对于其他更新服务器资源的请求, 如果其etags列表匹配, 服务器将执行更新, 反之, 将返回412(Precondition Failed)状态码的响应.
Last-Modified
语法: Last-Modified: 星期,日期 月份 年份 时:分:秒 GMT
Last-Modified: Tue, 04 Apr 2017 10:01:15 GMT
用于标记请求资源的最后一次修改时间, 格式为GMT(格林尼治标准时间). 如可用 new Date().toGMTString()
获取当前GMT时间. Last-Modified 是 ETag 的fallback机制, 优先级比 ETag 低, 且只能精确到秒, 因此不太适合短时间内频繁改动的资源. 不仅如此, 服务器端的静态资源, 通常需要编译打包, 可能出现资源内容没有改变, 而Last-Modified却改变的情况.
If-Modified-Since
语法同上, 如:
If-Modified-Since: Tue, 04 Apr 2017 10:12:27 GMT
缓存校验字段, 其值为上次响应头的Last-Modified值, 若与请求资源当前的Last-Modified值相同, 那么将返回304状态码的响应, 反之, 将返回200状态码响应.
If-Unmodified-Since
缓存校验字段, 语法同上. 表示资源未修改则正常执行更新, 否则返回412(Precondition Failed)状态码的响应. 常用于如下两种场景:
- 不安全的请求, 比如说使用post请求更新wiki文档, 文档未修改时才执行更新.
- 与 If-Range 字段同时使用时, 可以用来保证新的片段请求来自一个未修改的文档.
强缓存
一旦资源命中强缓存, 浏览器便不会向服务器发送请求, 而是直接读取缓存. Chrome下的现象是 200 OK (from disk cache)
或者 200 OK (from memory cache)
. 如下:
对于常规请求, 只要存在该资源的缓存, 且Cache-Control:max-age 或者expires没有过期, 那么就能命中强缓存.
协商缓存
缓存过期后, 继续请求该资源, 对于现代浏览器, 拥有如下两种做法:
- 根据上次响应中的ETag_value, 自动往request header中添加
If-None-Match
字段. 服务器收到请求后, 拿If-None-Match
字段的值与资源的ETag
值进行比较, 若相同, 则命中协商缓存, 返回304响应. - 根据上次响应中的Last-Modified_value, 自动往request header中添加
If-Modified-Since
字段. 服务器收到请求后, 拿If-Modified-Since
字段的值与资源的Last-Modified
值进行比较, 若相同, 则命中协商缓存, 返回304响应.
以上, ETag优先级比Last-Modified高, 同时存在时, 前者覆盖后者. 下面通过实例来理解下强缓存和协商缓存.
如下忽略首次访问, 第二次通过 If-Modified-Since
命中了304协商缓存.
协商缓存的响应结果, 不仅验证了资源的有效性, 同时还更新了浏览器缓存. 主要更新内容如下:
Age:0
Cache-Control:max-age=600
Date: Wed, 05 Apr 2017 13:09:36 GMT
Expires:Wed, 05 Apr 2017 00:55:35 GMT
Age:0
表示命中了代理服务器的缓存, age值为0表示代理服务器刚刚刷新了一次缓存.
Cache-Control:max-age=600
覆盖 Expires
字段, 表示从Date_value, 即 Wed, 05 Apr 2017 13:09:36 GMT
起, 10分钟之后缓存过期. 因此10分钟之内访问, 将会命中强缓存, 如下所示:
当然, 除了上述与缓存直接相关的字段外, http header中还包括如下间接相关的字段.
Age
出现此字段, 表示命中代理服务器的缓存. 它指的是代理服务器对于请求资源的已缓存时间, 单位为秒. 如下:
Age:2383321
Date:Wed, 08 Mar 2017 16:12:42 GMT
以上指的是, 代理服务器在2017年3月8日16:12:42时向源服务器发起了对该资源的请求, 目前已缓存了该资源2383321秒.
Date
指的是响应生成的时间. 请求经过代理服务器时, 返回的Date未必是最新的, 通常这个时候, 代理服务器将增加一个Age字段告知该资源已缓存了多久.
Vary
对于服务器而言, 资源文件可能不止一个版本, 比如说压缩和未压缩, 针对不同的客户端, 通常需要返回不同的资源版本. 比如说老式的浏览器可能不支持解压缩, 这个时候, 就需要返回一个未压缩的版本; 对于新的浏览器, 支持压缩, 返回一个压缩的版本, 有利于节省带宽, 提升体验. 那么怎么区分这个版本呢, 这个时候就需要Vary了.
服务器通过指定Vary: Accept-Encoding
, 告知代理服务器, 对于这个资源, 需要缓存两个版本: 压缩和未压缩. 这样老式浏览器和新的浏览器, 通过代理, 就分别拿到了未压缩和压缩版本的资源, 避免了都拿同一个资源的尴尬.
Vary:Accept-Encoding,User-Agent
如上设置, 代理服务器将针对是否压缩和浏览器类型两个维度去缓存资源. 如此一来, 同一个url, 就能针对PC和Mobile返回不同的缓存内容.
怎么让浏览器不缓存静态资源
实际上, 工作中很多场景都需要避免浏览器缓存, 除了浏览器隐私模式, 请求时想要禁用缓存, 还可以设置请求头: Cache-Control: no-cache, no-store, must-revalidate
.
当然, 还有一种常用做法: 即给请求的资源增加一个版本号, 如下:
<link rel="stylesheet" type="text/css" href="../css/style.css?version=1.8.9"/>
这样做的好处就是你可以自由控制什么时候加载最新的资源.
不仅如此, HTML也可以禁用缓存, 即在页面的\<head\>节点中加入\<meta\>标签, 代码如下:
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate"/>
上述虽能禁用缓存, 但只有部分浏览器支持, 而且由于代理不解析HTML文档, 故代理服务器也不支持这种方式.
IE8的异常表现
实际上, 上述缓存有关的规律, 并非所有浏览器都完全遵循. 比如说IE8.
资源缓存是否有效相关.
浏览器 | 前提 | 操作 | 表现 | 正常表现 |
---|---|---|---|---|
IE8 | 资源缓存有效 | 新开一个窗口加载网页 | 重新发送请求(返回200) | 展示缓存的页面 |
IE8 | 资源缓存失效 | 原浏览器窗口中单击 Enter 按钮 | 展示缓存的页面 | 重新发送请求(返回200) |
Last-Modified / E-Tag 相关.
浏览器 | 前提 | 操作 | 表现 | 正常表现 |
---|---|---|---|---|
IE8 | 资源内容没有修改 | 新开一个窗口加载网页 | 浏览器重新发送请求(返回200) | 重新发送请求(返回304) |
IE8 | 资源内容已修改 | 原浏览器窗口中单击 Enter 按钮 | 浏览器展示缓存的页面 | 重新发送请求(返回200) |
版权声明:转载需注明作者和出处。
本文作者:louis
本文链接:http://louiszhai.github.io/2017/04/07/http-cache/
参考文章
- Cache Policy Interaction—Maximum Age and Maximum Staleness
- HTTP/1.1: Header Field Definitions
- http - What’s the difference between Cache-Control: max-age=0 and no-cache? - Stack Overflow
- App 缓存方案:Http 缓存 · baitouwei
- Cache-Control - HTTP | MDN
- 彻底弄懂 Http 缓存机制 基于缓存策略三要素分解法
Http 缓存剖析的更多相关文章
- Redis分布式缓存剖析及大厂面试精髓v6.2.6
概述 官方说明 Redis官网 https://redis.io/ 最新版本6.2.6 Redis中文官网 http://www.redis.cn/ 不过中文官网的同步更新维护相对要滞后不少时间,但对 ...
- HTTP缓存剖析
web浏览器会自动缓存访问过的页面,当访问同一个页面的请求时,浏览器不再从服务器中重新下载页面而是优先使用本地缓存中的页面 为什么要进行web缓存 从用户的角度来看web缓存加快了上网速度,当然这是用 ...
- 剖析OkHttp缓存机制
现在应用市场上的 App 无一不需要网络操作,这些应用的开发者大多数都选择结合使用 OkHttp 和 Retrofit 来完成网络操作.okHttp 最为人称道的一个特性就是它的缓存机制,而我将在本篇 ...
- WCF技术剖析之八:ClientBase<T>中对ChannelFactory<T>的缓存机制
原文:WCF技术剖析之八:ClientBase<T>中对ChannelFactory<T>的缓存机制 和传统的分布式远程调用一样,WCF的服务调用借助于服务代理(Service ...
- taobao-pamirs-proxycache开源缓存代理框架实现原理剖析
写在前面 taobao-pamirs-proxycache 是一款开源缓存代理框架, 它将 缓存代码 与 业务代码 解耦.让开发专注coding业务, 缓存通过xml配置即可实现.本文先从此工具如何使 ...
- 全面剖析Smarty缓存机制一[三种缓存方式]
今天主要全面总结下Smarty模板引擎中强大的缓存机制,缓存机制有效减少了系统对服务器的压力,而这也是很多开发者喜欢Smarty的原因之一,由于篇幅较大,便于博友阅读,这篇文章将剖析Smarty缓存的 ...
- HDFS集中式的缓存管理原理与代码剖析--转载
原文地址:http://yanbohappy.sinaapp.com/?p=468 Hadoop 2.3.0已经发布了,其中最大的亮点就是集中式的缓存管理(HDFS centralized cache ...
- HDFS集中式的缓存管理原理与代码剖析
转载自:http://www.infoq.com/cn/articles/hdfs-centralized-cache/ HDFS集中式的缓存管理原理与代码剖析 Hadoop 2.3.0已经发布了,其 ...
- Mybatis架构原理(二)-二级缓存源码剖析
Mybatis架构原理(二)-二级缓存源码剖析 二级缓存构建在一级缓存之上,在收到查询请求时,Mybatis首先会查询二级缓存,若二级缓存没有命中,再去查询一级缓存,一级缓存没有,在查询数据库; 二级 ...
随机推荐
- 京东宙斯平台使用方法(accesstoken,appkey,appsecret参数和SDK的获取)
1.注册成为开发者 链接:https://dev.jd.com/ 2.创建应用 3.获取appsrecet和appkey,SDK(获取) 新建应用之后在左边应用证书栏位可以查看到appkey,apps ...
- Python数据分析练手:分析知乎大V
原文链接:https://zhuanlan.zhihu.com/p/92768131?utm_source=tuicool&utm_medium=referral 知乎,可以说是国内目前最大的 ...
- websocket抓包
https://www.cnblogs.com/xiaoniuzai/p/7588739.html http://blog.sina.com.cn/s/blog_12df1b9e60102vyeq.h ...
- HTTP最常见的请求头
HTTP最常见的请求头如下: l Accept:浏览器可接受的MIME类型: l Accept-Charset:浏览器可接受的字符集: l Accept ...
- Hystrix的介绍(断路、降级)
在大中型分布式系统中,通常系统很多依赖(HTTP,hession,Netty,Dubbo等),如下图: 在高并发访问下,这些依赖的稳定性与否对系统的影响非常大,但是依赖有很多不可控问题:如网络 ...
- Python习题002
作业1:判断某一个字符串是否是小数 def is_float(string): string1 = str(string) if string1.count('.') > 1: #检测字符串小数 ...
- 探索grafana
因为zabbix的监控图形不够美观,功能也不够强大, 那么就用到了grafana 填写zabbix插件配置: 如下需要根据主机群组和主机名来完成图形: grafana报警如下: 解决如下: 更改标准设 ...
- 剑指offer(9)——用两个栈实现队列
题目: 用两个栈实现一个队列.队列的声明如下,请实现它的两个函数appendTail和deleteHead,分别完成在队列尾部插入结点和在队列头部删除结点的功能. 思路: 首先定义两个栈stack1. ...
- BZOJ2555 SubString(后缀自动机+LCT)
询问串放在SAM上不跳fail跑到的节点的|right|即为答案.用LCT维护parent树即可.可以直接维护子树信息,也可以转化为路径加.注意强制在线所使用的mask是作为参数传进去的. #incl ...
- (十二) web服务与javaweb结合(3)
一.需求 上一章节虽然将webservice和web项目绑定在了一起,但是还是不能共同一个端口,本章讲解webservice和web项目绑定且共同端口. 二.案例 2.1 创建web工程,并引入依赖 ...