浏览器缓存机制及实现方式

什么是缓存

缓存是一种保存资源副本并在下次请求时直接使用该副本的技术。(MDN)javascript

这里的web浏览器缓存主要指http缓存。css

缓存的做用

  • 减小网络延迟,加快页面打开速度
  • 减小网络带宽消耗
  • 下降服务器压力

缓存分类

按使用权限分

  • 私有缓存,私有缓存只能用于单独用户
  • 共享缓存,共享缓存存储的响应可以被多个用户使用,例如,ISP 或你所在的公司可能会架设一个 web 代理来做为本地网络基础的一部分提供给用户。这样热门的资源就会被重复使用,减小网络拥堵与延迟。

按存储位置分

浏览器本地缓存

内存缓存(from memory cache)

内存缓存具备两个特色,分别是快速读取和时效性:快速读取:内存缓存会将编译解析后的文件,直接存入该进程的内存中,占据该进程必定的内存资源,以方便下次运行使用时的快速读取。时效性:一旦该进程关闭,则该进程的内存则会清空。html

硬盘缓存(from disk cache)

硬盘缓存则是直接将缓存写入硬盘文件中,读取缓存须要对该缓存存放的硬盘文件进行I/O操做,而后从新解析该缓存内容,读取复杂,速度比内存缓存慢。java

以浏览器中显示的说明为列:web

浏览器缓存图片1.png

  • from memory cache,表明使用内存中的缓存
  • from disk cache,表明使用的是硬盘中的缓存
  • from prefetch cache ,在 preload 或 prefetch 的资源加载时,二者也是均存储在 http cache,当资源加载完成后,若是资源是能够被缓存的,那么其被存储在 http cache 中等待后续使用;若是资源不可被缓存,那么其在被使用前均存储在 memory cache。

浏览器资源缓存访问优先级

浏览器请求一个资源时,会按照优先级(Service Worker -> Memory Cache -> Disk Cache -> Push Cache)依次查找缓存,若是命中则使用缓存,不然发起请求。算法

以未开通Service Worker的服务访问为例:http://baidu.com–> 200 –> 关闭页面的标签页 –> 从新打开http://baidu.com –> 200(from disk cache) –> 刷新 –> 200(from memory cache)。编程

代理/服务端缓存

网关缓存、CDN、反向代理缓存、负载均衡器等部署在服务器上的缓存浏览器

以腾讯 CDN 为例:请求头中 X-Cache-Lookup:Hit From MemCache 表示命中 CDN 节点的内存;X-Cache-Lookup:Hit From Disktank 表示命中 CDN 节点的磁盘;X-Cache-Lookup:Hit From Upstream 表示没有命中 CDN。缓存

浏览器资源访问流程

image2021-4-26_11-48-12.png

关键步骤以下:服务器

  1. 浏览器发送请求前,会先去缓存里查看是否命中强缓存,若是命中,则直接从缓存中读取资源,不会发送请求到服务器。不然,进入下一步。
  2. 当强缓存没有命中时,浏览器向服务器发起请求。
  3. 服务器会根据 Request Header 中的一些字段来判断是否命中协商缓存。若是命中,服务器会返回 304 响应,可是不会携带任何响应实体,只是告诉浏览器能够直接从浏览器缓存中获取这个资源。
  4. 若是本地缓存和协商缓存都没有命中,则从直接从服务器加载资源,服务器会将缓存规则放入HTTP响应报文的HTTP头中和请求结果一块儿返回给浏览器

Http协议头缓存相关字段

强缓存(本地缓存)控制Expires&Cache-Control

制强制缓存的字段分别是请求头中的Expires和,其中Cache-Control优先级比Expires高。

Expires(响应头)+Date

Expires是HTTP/1.0控制网页缓存的响应头字段,其值为服务器返回该请求结果缓存的到期时间,即再次发起该请求时,若是客户端的时间小于Expires的值时,直接使用缓存结果。

Expires是HTTP/1.0的字段,可是如今浏览器默认使用的是HTTP/1.1,那么在HTTP/1.1中网页缓存仍是否由Expires控制?

到了HTTP/1.1,Expire已经被Cache-Control替代,缘由在于Expires控制缓存的原理是使用客户端的时间与服务端返回的时间作对比,那么若是客户端与服务端的时间由于某些缘由(例如时区不一样;客户端和服务端有一方的时间不许确)发生偏差,那么强制缓存则会直接失效,这样的话强制缓存的存在则毫无心义。

Cache-Control(请求头和响应头都支持这个属性)

HTTP/1.1定义的 Cache-Control 头用来区分对缓存机制的支持状况, 请求头和响应头都支持这个属性。经过它提供的不一样的值来定义缓存策略。

  • Cache-Control: no-store   //缓存中不得存储任何关于客户端请求和服务端响应的内容。每次由客户端发起的请求都会下载完整的响应内容。
  • Cache-Control: no-cache //强制要求缓存把请求提交给原始服务器进行验证(协商缓存验证)。每次有请求发出时,缓存会将此请求发到服务器(该请求应该会带有与本地缓存相关的验证字段),服务器端会验证请求中所描述的缓存是否过时,若未过时(注:实际就是返回304),则缓存才使用本地缓存副本。
  • Cache-Control: private // "private" 则表示该响应是专用于某单个用户的,中间人不能缓存此响应,该响应只能应用于浏览器私有缓存中。
  • Cache-Control: public  //"public" 指令表示该响应能够被任何中间人(译者注:好比中间代理、CDN等)缓存。若指定了"public",则一些一般不被中间人缓存的页面(译者注:由于默认是private)(好比 带有HTTP验证信息(账号密码)的页面 或 某些特定状态码的页面),将会被其缓存。
  • Cache-Control: max-age=31536000   //表示资源可以被缓存(保持新鲜)的最大时间。相对Expires而言,max-age是距离请求发起的时间的秒数。针对应用中那些不会改变的文件,一般能够手动设置必定的时长以保证缓存有效,例如图片、css、js等静态资源。与Age配合使用
  • Cache-Control: must-revalidate    //缓存在考虑使用一个陈旧的资源时,必须先验证它的状态,已过时的缓存将不被使用

Pragma(请求头)

Pragma 是HTTP/1.0标准中定义的一个header属性,请求中包含Pragma的效果跟在头信息中定义Cache-Control: no-cache相同,可是HTTP的响应头没有明肯定义这个属性,因此它不能拿来彻底替代HTTP/1.1中定义的Cache-control头。一般定义Pragma以向后兼容基于HTTP/1.0的客户端。

协商缓存控制

协商缓存的标识也是在响应报文的HTTP头中和请求结果一块儿返回给浏览器的,控制协商缓存的字段分别有:Last-Modified / If-Modified-Since和Etag / If-None-Match,其中Etag / If-None-Match的优先级比Last-Modified / If-Modified-Since高。

Last-Modified(响应头)与 If-Modified-Since(请求头)

属于 http 1.0。当带着 If-Modified-Since 头访问服务器请求资源时,服务器会检查 Last-Modified,若是 Last-Modified 的时间早于或等于 If-Modified-Since 则会返回一个不带主体的 304 响应,不然将从新返回资源。

Last-Modified只能精确到一秒,能够做为一种弱校验器。

ETag(响应头) 与 If-None-Match(请求头)

属于 http 1.1。ETag 是一个响应首部字段,强校验器,它是根据实体内容生成的一段 hash 字符串,标识资源的状态,由服务端产生。If-None-Match 是一个条件式的请求首部。若是请求资源时在请求首部加上这个字段,值为以前服务器端返回的资源上的 ETag,则当且仅当服务器上没有任何资源的 ETag 属性值与这个首部中列出的时候,服务器才会返回带有所请求资源实体的 200 响应,不然服务器会返回不带实体的 304 响应。

ETag  VS  Last-Modified 

Last-Modified 标注的最后修改只能精确到秒级,若是某些文件在 1 秒钟之内,被修改屡次的话,它将不能准确标注文件的新鲜度;
某些文件也许会周期性的更改,可是他的内容并不改变(仅仅改变的修改时间),但 Last-Modified 却改变了,致使文件无法使用缓存;
有可能存在服务器没有准确获取文件修改时间,或者与代理服务器时间不一致等情形。
ETag 优先级比 Last-Modified 高,同时存在时会以 ETag 为准。

Vary (响应头)

是一个HTTP响应头部信息,它决定了对于将来的一个请求头,应该用一个缓存的回复(response)仍是向源服务器请求一个新的回复。它被服务器用来代表在 content negotiation algorithm(内容协商算法)中选择一个资源表明的时候应该使用哪些头部信息(headers)。它表示某个响应因某个响应头部而不一样。

好比 Vary: Accept 的意思即为,响应因请求资源格式头部而不一样,那么经过相同 URI 访问的资源就能够根据这个头上知道其内容格式不一样。

同一个 URL 能够提供多份不一样的文档,这就要求服务端和客户端之间有一个选择最合适版本的机制,这叫作内容协商。服务端根据客户端发送的请求头中某些字段自动发送最合适的版本。能够用于这个机制的请求头字段又分两种:内容协商专用字段(Accept 字段)、其余字段。

例如:Accept-Encoding 属于内容协商专用字段,服务端只须要在响应头中增长 Content-Encoding 字段,用来指明内容压缩格式;或者不输出 Content-Encoding 代表内容未通过压缩。缓存服务器,针对不一样的 Content-Encoding 缓存不一样内容,再根据具体请求中的 Accept-Encoding 字段返回最合适的版本。增长 Vary: Accept-Encoding 响应头,明确告知缓存服务器按照 Accept-Encoding 字段的内容,分别缓存不一样的版本;

不一样资源缓存策略

  • 不一样的资源可能有不一样的更新要求。审查并肯定每一个资源适合的 max-age;
  • 有些资源的更新比其余资源频繁。若是资源的特定部分(例如 JS 函数或一组 CSS 样式)会常常更新,应考虑将其代码做为单独的文件提供。这样,每次获取更新时,剩余内容(例如不会频繁更新的库代码)能够从缓存中获取,确保下载的内容量最少;
  • 对 HTML 文档组合使用包含内容特征码的资源网址以及短期或 no-cache 的生命周期,能够控制客户端获取更新的速度,低频更新的资源(js/css)变更了,只用在高频变更的资源文件(html)里作入口的改动。

特殊说明

F5/点击工具栏中的刷新按钮/右键菜单从新加载

F5的做用和直接在URI输入栏中输入而后回车是不同的,F5会让浏览器不管如何都发一个HTTP Request给Server,即便先前的响应中有Expires头部。因此,当我在 网页中按F5的时候,浏览器会发送一个HTTP Request给Server,可是包含这样的Headers:

Cache-Control: max-age=0
If-Modified-Since: Fri, 15 Jul 2016 04:11:51 GMT

其中Cache-Control是Chrome强制加上的,而If-Modified-Since是由于获取该资源的时候包含了Last-Modified头部,浏览器会使用If-Modified-Since头部信息从新发送该时间以确认资源是否须要从新发送。 实际上Server没有修改这个index.css文件,因此返回了一个304(Not Modified),这样的响应信息很小,所消耗的route-trip很少,网页很快就刷新了。
浏览器缓存图片3.png

上面的例子中没有ETag,若是Response中包含ETag,F5引起的Http Request中也是会包含If-None-Match的。

缓存规则实现

以上描述的客户端浏览器缓存是指存储位置在客户端浏览器, 可是对客户端浏览器缓存的实际设置工做是在服务器上的资源中完成的. 虽然上面介绍了有关于客户端浏览器缓存的属性, 可是实际上对这些属性的设置工做都须要在服务器的资源中作设置. 一般有两种操做手段对浏览器缓存进行设置, 一个是经过指令声明来设置, 另一个是经过编程方式来设置.

Ngnix指令设置

例子1
配置指令expires,能够控制 HTTP 响应中的Expires和Cache-Controle的值,默认是 off

location ~ .*.(js|css)?$ {    
    expires 1y; }

例2:
对全部后缀为.html 的请求,返回头 cache-control 使用 no-cache 指令

location ~ .*\.(html)$ {
         try_files $uri $uri/ =404;
         root /data/web/default;
         add_header Cache-Control no-cache;
}

例3:
etag设置

http {
        etag off;

例4

配置last-modified(默认开启)

编程方式

这里再也不详细描述,不一样语言做出的server服务可查看相关模块说明
以koa实现为例:

//koastart
var koa = require('koa');
var app = new koa();
// response
app.use(function *(){
  this.body = 'Hello World';
  var etag = this.get('ETag');
  console.log("etag:"+etag);
  var date = new Date;
  var hashStr = this.body;
  var hash = require("crypto").createHash('sha1').update(hashStr).digest('base64');
  this.set({
    'Cache-Control':'max-age=120',
    'Etag': hash,
    'Last-Modified': new Date
  });
});
app.listen(3000);

参考文档

MDN-http缓存:https://developer.mozilla.org... ;
HTTP缓存控制:https://imweb.io/topic/5795dc... ;
https://web.dev/http-cache/ ;
https://mp.weixin.qq.com/s/d2... ;
https://www.jiqizhixin.com/ar... ;