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

lizziebing

什么是缓存

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

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

缓存的作用

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

缓存分类

按使用权限分

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

按存储位置分

浏览器本地缓存

内存缓存(from memory cache)

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

硬盘缓存(from disk cache)

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

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

浏览器缓存图片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... ;

阅读 154

白底黑字
黑白之间是无限的绚烂
79 声望
4 粉丝
0 条评论
你知道吗?

79 声望
4 粉丝
宣传栏