最近在项目中遇到了 IE 浏览器因缓存问题未能成功向后端发送 GET类型请求 的 bug,然后顺藤摸瓜顺便看了看缓存的知识,觉得有必要总结跟大家分享一下。

在前端开发中,性能一直都是被大家所重视的一点,然而判断一个网站的性能最直观的就是看网页打开的速度。其中提高网页反应速度的一个方式就是使用缓存。一个优秀的缓存策略可以缩短网页请求资源的距离,减少延迟,并且由于缓存文件可以重复利用,还可以减少带宽,降低网络负荷。

1. 介绍

Web 缓存是指一个 Web 资源(如 html 页面,图片,js,数据等)存在于 Web 服务器和客户端(浏览器)之间的副本。

缓存会根据进来的请求保存输出内容的副本;当下一个请求来到的时候,如果是相同的 URL,缓存会根据缓存机制决定是直接使用副本响应访问请求,还是向源服务器再次发送请求。比较常见的就是浏览器会缓存访问过网站的网页,当再次访问这个 URL 地址的时候,如果网页没有更新,就不会再次下载网页,而是直接使用本地缓存的网页。只有当网站明确标识资源已经更新,浏览器才会再次下载网页。至于浏览器和网站服务器是如何标识网站页面是否更新的机制,将在后面介绍。

1.1 Web 缓存的作用

Web 缓存的作用显而易见:

  • 减少网络带宽消耗:无论对于网站运营者或者用户,带宽都代表着金钱,过多的带宽消耗,只会便宜了网络运营商。当 Web 缓存副本被使用时,只会产生极小的网络流量,可以有效的降低运营成本。
  • 降低服务器压力:给网络资源设定有效期之后,用户可以重复使用本地的缓存,减少对源服务器的请求,间接降低服务器的压力。同时,搜索引擎的爬虫机器人也能根据过期机制降低爬取的频率,也能有效降低服务器的压力。
  • 减少网络延迟,加快页面打开速度:带宽对于个人网站运营者来说是十分重要,而对于大型的互联网公司来说,可能有时因为钱多而真的不在乎。那 Web 缓存还有作用吗?答案是肯定的,对于最终用户,缓存的使用能够明显加快页面打开速度,达到更好的体验。

1.2 Web 缓存的类型

Web 缓存大致可以分为以下几种类型 详细内容:

  • 数据库数据缓存
  • 服务器端缓存
  • 浏览器端缓存
  • Web 应用层缓存

浏览器通过代理服务器向源服务器发起请求的原理如下图:

image-24

浏览器先向代理服务器发起 Web 请求,再将请求转发到源服务器。它属于共享缓存,所以很多地方都可以使用其缓存资源,因此对于节省流量有很大作用。

浏览器缓存是将文件保存在客户端,在同一个会话过程中会检查缓存的副本是否足够新,在后退网页时,访问过的资源可以从浏览器缓存中拿出使用。通过减少服务器处理请求的数量,用户将获得更快的体验

下面着重关注一下浏览器缓存。

2. Web 缓存的工作原理

所有的缓存都是基于一套规则来帮助他们决定什么时候使用缓存中的副本提供服务(假设有副本可用的情况下,未被销毁回收或者未被删除修改)。这些规则有的在协议中有定义(如 HTTP 协议1.0和1.1),有的则是由缓存的管理员设置(如 DBA、浏览器的用户、代理服务器管理员或者应用开发者)。

2.1 浏览器端的缓存规则

对于浏览器端的缓存来讲,这些规则是在 HTTP 协议头和 HTML 页面的 Meta标签中定义的。他们分别从新鲜度校验值两个维度来规定浏览器是直接使用缓存中的副本,还是需要去源服务器获取更新的版本。

新鲜度(过期机制):也就是缓存副本有效期。一个缓存副本必须满足以下任一条件,浏览器会认为它是有效的,足够新的,而直接从缓存中获取副本并渲染:

  • 含有完整的过期时间控制头信息(HTTP 协议报头),并且仍在有效期内
  • 浏览器已经使用过这个缓存副本,并且在一个会话中已经检查过新鲜度

校验值(验证机制):服务器返回资源的时候有时在控制头信息带上这个资源的实体标签 Etag(Entity Tag),它可以用来作为浏览器再次请求过程的校验标识。如过发现校验标识不匹配,说明资源已经被修改或过期,浏览器需求重新获取资源内容。

2.2 浏览器缓存的控制

2.2.1 使用 HTML 的 Meta 标签

<META HTTP-EQUIV="Pragma" CONTENT="no-cache">

上述代码的作用是告诉浏览器当前页面不被缓存,每次访问都需要去服务器拉取。使用上很简单,但只有部分浏览器可以支持,而且所有缓存代理服务器都不支持,因为代理不解析 HTML 内容本身。可以通过这个页面测试你的浏览器是否支持:[Pragma No-Cache Test] (http://www.procata.com/cachetest/tests/pragma/index.php)。

2.2.2 使用缓存有关的 HTTP 消息报头

一个 URI 的完整 HTTP 协议交互过程是由 HTTP 请求和 HTTP 响应组成的。有关 HTTP 详细内容可参考《Hypertext Transfer Protocol — HTTP/1.1》、《HTTP 协议详解》等。

在 HTTP 请求和响应的消息报头中,常见的与缓存有关的消息报头有:

image-15

稍微解释一下:

(1) Cache-Control

max-age(单位为 s)指定设置缓存最大的有效时间,定义的是时间长短。当浏览器向服务器发送请求后,在 max-age 这段时间里浏览器就不会再向服务器发送请求了。我们来找个资源看下。比如 QQ 推广上的 css 资源,max-age=3600,也就是说缓存有效期为 3600 秒(也就是 1h)。于是在1天内都会使用这个版本的资源,即使服务器上的资源发生了变化,浏览器也不会得到通知。max-age 会覆盖掉 Expires,后面会有讨论。

image-16


s-maxage(单位为 s)同 max-age,只用于共享缓存(比如 CDN 缓存)。比如,当 s-maxage=60 时,在这60秒中,即使更新了 CDN 的内容,浏览器也不会进行请求。也就是说 max-age 用于普通缓存,而 s-maxage 用于代理缓存。如果存在 s-maxage,则会覆盖掉 max-age 和 Expires header。

public 指定响应会被缓存,并且在多用户间共享。也就是下图的意思。如果没有指定 public 还是 private,则默认为 public。

image-17

private 响应只作为私有的缓存(见下图),不能在用户间共享。如果要求 HTTP 认证,响应会自动设置为 private。

image-18

no-cache 指定不缓存响应,表明资源不进行缓存,但是设置了 no-cache 之后并不代表浏览器不缓存,而是在获取缓存前要向服务器确认资源是否被更改。因此有的时候只设置 no-cache 防止缓存还是不够保险,还可以加上 private 指令,将过期时间设为过去的时间。

no-store 绝对禁止缓存,一看就知道如果用了这个命令当然就是不会进行缓存啦~每次请求资源都要从服务器重新获取。

must-revalidate 指定如果页面是过期的,则去服务器进行获取。这个指令并不常用,就不做过多的讨论了。

cache-control的种类这么多,然而怎么使用它们呢,参看下图:

image-19

(2) Expires

缓存过期时间,用来指定资源到期的时间,是服务器端的具体的时间点。也就是说,  Expires=max-age + 请求时间 ,需要和 Last-modified 结合使用。但在上面我们提到过,cache-control 的优先级更高。Expires 是 Web 服务器响应消息头字段,在响应 http 请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。

image-20

(3) Last-modified & If-modified-since

服务器端文件的最后修改时间,需要和 cache-control 共同使用,是检查服务器端资源是否更新的一种方式。当浏览器再次进行请求时,会向服务器传送If-Modified-Since报头,询问 Last-Modified 时间点之后资源是否被修改过。如果没有修改,则返回码为304,使用缓存;如果修改过,则再次去服务器请求资源,返回码和首次请求相同为200,资源为服务器最新资源。

(4) Etag & & If-None-Match

根据实体内容生成一段 hash 字符串,标识资源的状态,由服务端产生。浏览器会将这串字符串传回服务器,验证资源是否已经修改,如果没有修改,过程如下:

image-21

2.2.3 缓存报头种类与优先级

(1) Cache-Control与Expires

Cache-ControlExpires的作用一致,都是指明当前资源的有效期,控制浏览器是否直接从浏览器缓存取数据还是重新发请求到服务器取数据。只不过 Cache-Control的选择更多,设置更细致,如果同时设置的话,其优先级高于 Expires

(2) Last-Modified 与 ETag

你可能会觉得使用 Last-Modified 已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要 Etag(实体标识)呢?HTTP1.1 中 Etag 的出现主要是为了解决几个 Last-Modified 比较难解决的问题:

  • Last-Modified 标注的最后修改只能精确到级,如果某些文件在1秒钟以内,被修改多次的话,它将不能准确标注文件的新鲜度
  • 如果某些文件会被定期生成,当有时内容并没有任何变化,但 Last-Modified 却改变了,导致文件没法使用缓存
  • 有可能存在服务器没有准确获取文件修改时间,或者与代理服务器时间不一致等情形

Etag 是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符,能够更加准确的控制缓存。Last-Modified 与 ETag 是可以一起使用的,服务器会优先验证 ETag,一致的情况下,才会继续比对 Last-Modified,最后才决定是否返回304。Etag 的服务器生成规则和强弱 Etag 的相关内容可以参考,《互动百科-Etag》和《HTTP Header definition》,这里不再深入。

(3) Last-Modified/ETag 与 Cache-Control/Expires

配置 Last-Modified/ETag的情况下,浏览器再次访问统一 URI 的资源,还是会发送请求到服务器询问文件是否已经修改,如果没有,服务器会只发送一个304回给浏览器,告诉浏览器直接从自己本地的缓存取数据;如果修改过那就整个数据重新发给浏览器;

Cache-Control/Expires则不同,如果检测到本地的缓存还是有效的时间范围内,浏览器直接使用本地副本,不会发送任何请求。两者一起使用时, Cache-Control/Expires的优先级要高,即当本地副本根据 Cache-Control/Expires发现还在有效期内时,则不会再次发送请求去服务器询问修改时间 Last-Modified或实体标识 Etag了。

一般情况下,两者会配合一起使用,因为即使服务器设置缓存时间, 当用户点击“刷新”按钮时,浏览器会忽略缓存继续向服务器发送请求,这时 Last-Modified/ETag将能够很好利用304,从而减少响应开销。

2.2.4 哪些请求不能被缓存?

无法被浏览器缓存的请求:

  • HTTP 信息头中包含 Cache-Control:no-cache,pragma:no-cache,或 Cache-Control:max-age=0 等告诉浏览器不用缓存的请求
  • 需要根据 Cookie,认证信息等决定输入内容的动态请求是不能被缓存的
  • 经过 HTTPS 安全加密的请求(有人也经过测试发现,ie 其实在头部加入 Cache-Control:max-age 信息,firefox 在头部加入 Cache-Control:Public 之后,能够对 HTTPS 的资源进行缓存,参考《HTTPS 的七个误解》)
  • POST 请求无法被缓存
  • HTTP 响应头中不包含 Last-Modified/Etag,也不包含 Cache-Control/Expires 的请求无法被缓存

3. 使用缓存流程

一个用户发起一个静态资源请求的时候,浏览器会通过以下几步来获取并展示资源:

image-22

缓存行为主要由缓存策略决定,而缓存策略由内容拥有者设置。这些策略主要通过特定的 HTTP 头部来清晰地表达。

以上过程也可以被概括为三个阶段:

  • 本地缓存阶段:先在本地查找该资源,如果有发现该资源,而且该资源还没有过期,就使用这一个资源,完全不会发送http请求到服务器;
  • 协商缓存阶段:如果在本地缓存找到对应的资源,但是不知道该资源是否过期或者已经过期,则发一个 http 请求到服务器,然后服务器判断这个请求,如果请求的资源在服务器上没有改动过,则返回304,让浏览器使用本地找到的那个资源;
  • 缓存失败阶段:当服务器发现请求的资源已经修改过,或者这是一个新的请求(在本来没有找到资源),服务器则返回该资源的数据,并且返回200, 当然这个是指找到资源的情况下,如果服务器上没有这个资源,则返回404。

4. 用户操作行为与缓存的关系

用户在使用浏览器的时候,会有各种操作,比如输入地址后回车,按 F5 刷新等,这些行为会对缓存有什么影响呢?

image-23

通过上表我们可以看到,当用户在按 F5进行刷新的时候,会忽略 Expires/Cache-Control 的设置,会再次发送请求去服务器请求,而 Last-Modified/Etag 还是有效的,服务器会根据情况判断返回304还是200;

而当用户使用 Ctrl+F5进行强制刷新的时候,只是所有的缓存机制都将失效,重新从服务器拉去资源。

  • 普通刷新 – 当按下 F5 或者点击刷新按钮来刷新页面的时候,浏览器将绕过本地缓存来发送请求到服务器,此时,协商缓存是有效的
  • 强制刷新 – 当按下 ctrl+F5 来刷新页面的时候,浏览器将绕过各种缓存(本地缓存和协商缓存),直接让服务器返回最新的资源
  • 回车或转向 – 当在地址栏上输入回车或者按下跳转按钮的时候,所有缓存都生效

5. 如何从缓存角度改善站点

  • 同一个资源保证 URL 的稳定性
  • 给 css、js、图片等资源增加 HTTP 缓存头,并强制入口 html 不被缓存
  • 减少对 Cookie 的依赖
  • 多用 Get 方式请求动态 Cgi
  • 动态 CGI 也是可以被缓存