写在前面:以下文章对笔者理解浏览器缓存有较大的帮助,感谢作者的分享。
1、缓存的分类
缓存分为服务端侧(server side,比如 Nginx、Apache)和客户端侧(client side,比如 web browser)。
服务端缓存又分为代理服务器缓存和反向代理服务器缓存(也叫网关缓存,比如 Nginx反向代理、Squid等),其实广泛使用的 CDN 也是一种服务端缓存,目的都是让用户的请求走”捷径“,并且都是缓存图片、文件等静态资源。
客户端侧缓存一般指的是浏览器缓存,目的就是加速各种静态资源的访问,想想现在的大型网站,随便一个页面都是一两百个请求,每天 pv 都是亿级别,如果没有缓存,用户体验会急剧下降、同时服务器压力和网络带宽都面临严重的考验。
1.1、为什么需要缓存
Web缓存游走于服务器和客户端之间。这个服务器可能是源服务器也可能是代理服务器。
Web缓存就在服务器-客户端之间搞监控,监控请求,并且把请求输出的内容(例如html页面、 图片和文件)(统称为副本)另存一份;
然后,如果下一个请求是相同的URL,则直接请求保存的副本,而不是再次麻烦源服务器。
- 减少网络带宽消耗
无论对于网站运营者或者用户,带宽都代表着金钱,过多的带宽消耗,只会便宜了网络运营商。当Web缓存副本被使用时,只会产生极小的网络流量,可以有效的降低运营成本。
- 降低服务器压力
给网络资源设定有效期之后,用户可以重复使用本地的缓存,减少对源服务器的请求,间接降低服务器的压力。同时,搜索引擎的爬虫机器人也能根据过期机制降低爬取的频率,也能有效降低服务器的压力。
- 减少网络延迟,加快页面打开速度
带宽对于个人网站运营者来说是十分重要,而对于大型的互联网公司来说,可能有时因为钱多而真的不在乎。那Web缓存还有作用吗?答案是肯定的,对于最终用户,缓存的使用能够明显加快页面打开速度,达到更好的体验。
2、浏览器缓存机制详解
浏览器缓存控制机制有两种:HTML Meta标签 vs. HTTP头信息
HTTP头缓存策略分为两种:强缓存和协商缓存。
实现强缓存可以通过两种响应头实现:Expires 和 Cache-Control 。
协商缓存需要客户端和服务端共同实现,和强缓存一样,也有两种实现方式。
- Last-Modified 和 If-Modified-Since
- ETag 和 If-None-Match
2.1 HTML Meta标签控制缓存
浏览器缓存机制,其实主要就是HTTP协议定义的缓存机制(如: Expires; Cache-control等)。但是也有非HTTP协议定义的缓存机制,如使用HTML Meta 标签,Web开发者可以在HTML页面的<head>节点中加入<meta>标签,代码如下:
<META HTTP-EQUIV="Pragma" CONTENT="no-cache">
上述代码的作用是告诉浏览器当前页面不被缓存,每次访问都需要去服务器拉取。使用上很简单,但只有部分浏览器可以支持,而且所有缓存代理服务器都不支持,因为代理不解析HTML内容本身。而广泛应用的还是 HTTP头信息来控制缓存,下面我主要介绍HTTP协议定义的缓存机制。
2.2、浏览器请求流程控制
-
浏览器第一次请求流程图:
-
浏览器再次请求时:
2.3、 几个重要概念解释
-
Expires策略:Expires是Web服务器响应消息头字段。
在响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。
不过Expires 是HTTP 1.0的东西,现在默认浏览器均默认使用HTTP 1.1,所以它的作用基本忽略。
Expires 的一个缺点就是,返回的到期时间是服务器端的时间,这样存在一个问题,如果客户端的时间与服务器的时间相差很大(比如时钟不同步,或者跨时区),那么误差就很大,所以在HTTP 1.1版开始,使用Cache-Control: max-age=秒替代。
Expires头唯一的有效值是HTTP时间,其他值都会被认为是“前男友前女友”之类,不会去缓存的。注意:时间是格林威治时间(GMT),而不是本地时间。如下所示:
Expires: Fri, 30 Oct 1998 14:19:41 GMT
-
Cache-control策略(重点关注):Cache-Control与Expires的作用一致,都是指明当前资源在浏览器缓存时间的有效期,控制浏览器是否直接从浏览器缓存取数据还是重新发请求到服务器取数据。只不过Cache-Control的选择更多,设置更细致,如果同时设置的话,其优先级高于Expires。
-
Last-Modified/If-Modified-Since:Last-Modified/If-Modified-Since要配合Cache-Control使用。
Last-Modified:标示这个响应资源的最后修改时间。web服务器在响应请求时,告诉浏览器资源的最后修改时间。
I
f-Modified-Since:当资源过期时(使用
Cache-Control标识的max-age),发现资源具有Last-Modified声明,
则再次向web服务器请求时带上头 If-Modified-Since,表示请求时间。web服务器收到请求后发现有头If-Modified-Since 则与被请求资源的最后修改时间进行比对。
若最后修改时间较新,说明资源又被改动过,则响应整片资源内容(写在响应消息包体内),HTTP 200;
若最后修改时间较旧,说明资源无新修改,则响应HTTP 304 (无需包体,节省流量),告知浏览器继续使用所保存的cache。
- Etag/If-None-Match:Etag/If-None-Match也要配合Cache-Control使用。
Etag:web服务器响应请求时,告诉浏览器当前资源在服务器的唯一标识(生成规则由服务器决定)。
Apache中,ETag的值,默认是对文件的索引节(INode),大小(Size)和最后修改时间(MTime)进行Hash后得到的。
If-None-Match:当资源过期时(使用Cache-Control标识的max-age),发现资源具有Etage声明,则再次向web服务器请求时带上头If-None-Match (Etag的值)。
web服务器收到请求后发现有头If-None-Match 则与被请求资源的相应校验串进行比对,决定返回200或304。
- 既生Last-Modified何生Etag?
你可能会觉得使用Last-Modified已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要Etag(实体标识)呢?
HTTP1.1中Etag的出现主要是为了解决几个Last-Modified比较难解决的问题:
Last-Modified标注的最后修改只能精确到秒级,如果某些文件在1秒钟以内,被修改多次的话,它将不能准确标注文件的修改时间。
如果某些文件会被定期生成,当有时内容并没有任何变化,但Last-Modified却改变了,导致文件没法使用缓存有可能存在服务器没有准确获取文件修改时间,或者与代理服务器时间不一致等情形
Etag是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符,能够更加准确的控制缓存。
Last-Modified与ETag一起使用时,服务器会优先验证ETag。
-
yahoo的Yslow法则中则提示谨慎设置Etag:需要注意的是分布式系统里多台机器间文件的last-modified必须保持一致,以免负载均衡到不同机器导致比对失败,Yahoo建议分布式系统尽量关闭掉Etag(每台机器生成的etag都会不一样,因为除了 last-modified、inode 也很难保持一致)。
-
Pragma行是为了兼容HTTP1.0,作用与Cache-Control: no-cache是一样的。
-
最后总结下几种状态码的区别:
3、用户行为与缓存
浏览器缓存行为还有用户的行为有关,如果大家对强制刷新(Ctrl + F5) 还有印象的话应该能立刻明白我的意思~
用户操作 | Expires/Cache-Control | Last-Modified/Etag |
地址栏回车 | 有效 | 有效 |
页面链接跳转 | 有效 | 有效 |
新开窗口 | 有效 | 有效 |
前进、后退 | 有效 | 有效 |
F5/按钮刷新 | 无效(BR重置max-age=0) | 有效 |
Ctrl+F5刷新 | 无效(重置CC=no-cache) | 无效(请求头丢弃该选项)
|
4、服务器对Cache-Control的配置:
指令 | 说明 |
---|---|
public | 所有内容都被缓存 |
private | 仅客户端缓存代理服务器不缓存 |
no-cache | 必须先与代理服务器确认是否更改,然后在在决定使用缓存还是请求 |
no-store | 所有内容都不会被缓存 |
must-revalidation/proxy-revalidation | 如果缓存内容失效,请求必须发送服务器/代理进行验证 |
max-age=<s> | 缓存内容在s秒后失效,仅HTTP1.1可用 |
对于上述配置浏览器的响应为:
指令 | 打开新窗口 | 原窗口单击Enter按钮 | 刷新 | 点击返回按钮 |
public | 缓存页面 | 缓存页面 | 重新请求页面 | 缓存页面 |
private | 重新请求 | 第一次请求,随后缓存 | 重新请求页面 | 缓存页面 |
no-cache/no-store | 重新请求 | 重新请求 | 重新请求 | 缓存页面 |
must-revalidation/proxy-revalidation | 浏览器重新请求 | 第一次请求,随后缓存 | 重新请求 | 缓存页面 |
max-age=xxx | 在XX秒后重新请求 | 在XX秒后重新请求 | 重新请求 | 在xx秒后重新请求 |