浏览器和网络篇(一)--浏览器的缓存机制

2,077 阅读16分钟

http 全过程

输入url,从url解析出域名-->DNS映射为IP-->TCP三次握手(完成TLS/SSL握手)-->构造HTTP请求,填充上下文到HTTP首部--> 发起HTTP请求-->HTTP响应-->(浏览器跟踪重定向地址)-->服务器处理请求-->服务器返回一个html响应-->(视情况决定释放TCP连接)-->浏览器引擎解析HTML响应,渲染包体到用户界面-->获取页面内资源的HTTP请求。

image.png

DNS 缓存

什么是DNS

全称 Domain Name System ,即域名系统。

  • 万维网上作为域名和IP地址相互映射的一个分布式数据库,能够使用户更方便的访问互联网,而不用去记住能够被机器直接读取的IP数串。
  • DNS协议运行在UDP协议之上,使用端口号53。

dns解析

通过域名,最终得到该域名对应的IP地址的过程叫做域名解析(或主机名解析)

www.dnscache.com (域名) - DNS解析 -> 11.222.33.444 (IP地址)

dns缓存

浏览器、操作系统、本地Local DNS、根域名服务器,它们都会对DNS结果做一定程度的缓存。 dns查询过程:

  • 1、首先搜索浏览器自身的DNS缓存,如果存在,则域名解析到此完成。
  • 2、浏览器自身的缓存里面没有就会尝试读取操作系统的hosts文件看是否存在对应的映射关系,如果存在,则域名解析到此完成。
  • 3、如果本地hosts文件不存在映射关系,则查找本地DNS服务器(ISP服务器,或者自己手动设置的DNS服务器),如果存在,域名到此解析完成。
  • 如果本地DNS服务器还没找到的话,它就会向根服务器发出请求,进行递归查询

CDN 缓存

什么是CDN?

全称 Content Delivery Network,即内容分发网络. 用户在浏览网站的时候,CDN会选择一个离用户最近的CDN边缘节点来响应用户的请求,访问延时大大降低,这样可以起到分流作用,减轻服务器负载压力。

CDN缓存

  • 关于CDN缓存,在浏览器本地缓存失效后,浏览器会向CDN边缘节点发起请求。

  • 类似浏览器缓存,CDN边缘节点也存在着一套缓存机制。CDN边缘节点缓存策略因服务商不同而不同,但一般都会遵循http标准协议,通过http响应头中的 Cache-control: max-age 的字段来设置CDN边缘节点数据缓存时间.

  • 当浏览器向CDN节点请求数据时,CDN节点会判断缓存数据是否过期,若缓存数据并没有过期,则直接将缓存数据返回给客户端;

  • 否则,CDN节点就会向服务器发出回源请求,从服务器拉取最新数据,更新本地缓存,并将最新数据返回给客户端。 CDN服务商一般会提供基于文件后缀、目录多个维度来指定CDN缓存时间,为用户提供更精细化的缓存管理。

CDN 优势

CDN节点解决了跨运营商和跨地域访问的问题,访问延时大大降低。 大部分请求在CDN边缘节点完成,CDN起到了分流作用,减轻了源服务器的负载。

浏览器缓存(http缓存)

1.png 浏览器与服务器通信的方式为应答模式,即是:浏览器发起HTTP请求 – 服务器响应该请求。

  • 那么浏览器怎么确定一个资源该不该缓存,如何去缓存呢?
  • 浏览器第一次向服务器发起该请求后拿到请求结果后,将请求结果和缓存标识存入浏览器缓存,浏览器对于缓存的处理是根据第一次请求资源时返回的响应头来确定的。具体过程如下图: image.png

浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识 浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中

浏览器缓存的优点

1.减少了冗余的数据传输 比如直接使用缓存而不发起请求

2.减少了服务器的负担,大大提升了网站的性能,比如发起了请求但是和服务器缓存数据一致不需要重新响应

3.加快了客户端加载网页的速度 比如一个数据请求,可以分为浏览器发起请求、服务求响应请求、浏览器解析响应三个步骤。浏览器缓存可以帮助我们在第一和第二步骤中优化性能,

缓存位置分类

从缓存位置上来说分为四种,并且各自有优先级,当依次查找缓存且都没有命中的时候,才会去请求网络。

  • Service Worker
  • Memory Cache
  • Disk Cache
  • Push Cache

1.Service Worker

  • Service Worker: 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。
  • 传输协议 HTTPS : Service Worker涉及请求拦截,必须使用 HTTPS 协议来保障安全。
  • 可以自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。

Service Worker 实现缓存功能一般分为三个步骤:

1、首先注册 Service Worker。

2、监听到install 事件: 缓存需要的文件,下次访问时候通过拦截请求查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。

3、当 Service Worker 没有命中缓存的时候,调用 fetch 函数获取数据,根据缓存查找优先级去查找数据。 4、Memory Cache或则网络请求中获取的数据,浏览器还是会显示我们是从 Service Worker 中获取的内容。

2.Memory Cache 内存缓存

  • 读取数据肯定比磁盘快,高效,但是缓存持续性很短,会随着进程(比如关闭tab页面)的释放而释放,
  • 容量小,操作系统需要精打细算内存的使用。
  • 二次刷新页面的数据大都来自于内存缓存

目前Webkit资源分成两类:

  • 一类是主资源,比如HTML页面,或者下载项
  • 一类是派生资源,比如HTML页面中内嵌的图片或者脚本链接
  • 分别对应代码中两个类:MainResourceLoader和SubresourceLoader。

虽然Webkit支持memoryCache,但是也只是针对派生资源,它对应的类为CachedResource,用于保存原始数据(比如CSS,JS等),以及解码过的图片数据。

1、内存缓存资源指令:preloader相关指令 (例如<link rel="prefetch">)下载资源, 是页面优化的常见手段之一解析js/css文件,边网络请求下一个资源

2、内存缓存资源时和HTTP缓存头Cache-Control关系不大,资源的匹配 除了对URL做匹配,还可能会对Content-TypeCORS等其他特征做校验。

3.Disk Cache

Disk Cache

  • 存储在硬盘中,读取慢,什么都能存储,比Memory Cache效率低但是容量大。
  • 覆盖面大。
  • 根据 HTTP Herder 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求,它的直接操作对象为CurlCacheManager
  • 跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。
  • 绝大部分的缓存都来自 Disk Cache

文件一般优先存储进硬盘,对于系统优先级和使用率高的相对较小的资源存储在内存,

image.png

因为CSS文件加载一次就可渲染出来,我们不会频繁读取它,所以它不适合缓存到内存中,但是js之类的脚本却随时可能会执行,如果脚本在磁盘当中,我们在执行脚本的时候需要从磁盘取到内存中来,这样IO开销就很大了,有可能导致浏览器失去响应。

4.Push Cache

  • Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。

  • 它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在Chrome浏览器中只有5分钟左右

  • 不严格执行HTTP头中的缓存指令, 可以推送 no-cache 和 no-store 的资源,一旦连接被关闭,Push Cache 就被释放

  • 所有的资源都能被推送,并且能够被缓存

  • Edge 和 Safari 浏览器支持相对比较差

  • 多个页面可以使用同一个HTTP/2的连接,也就可以使用同一个Push Cache。这主要还是依赖浏览器的实现而定,出于对性能的考虑,有的浏览器会对相同域名但不同的tab标签使用同一个HTTP连接。

  • Push Cache 中的缓存只能被使用一次

  • 浏览器可以拒绝接受已经存在的资源推送,可以给其他域名推送资源。

如果以上四种缓存都没有命中的话,那么只能发起请求来获取资源了。

三级缓存原理 (访问缓存优先级)

  • 先在内存中查找,如果有,直接加载。
  • 如果内存中不存在,则在硬盘中查找,如果有直接加载。
  • 如果硬盘中也没有,那么就进行网络请求。
  • 请求获取的资源缓存到硬盘和内存。

缓存策略分类

那么为了性能上的考虑,大部分的接口都应该选择好缓存策略,通常浏览器缓存策略分为两种:强缓存和协商缓存 缓存策略都是通过设置 HTTP Header 来实现的。 浏览器再向服务器请求资源时,首先判断是否命中强缓存,再判断是否命中协商缓存!

强缓存

浏览器在加载资源时,会先根据本地缓存资源的 header 中的信息判断是否命中强缓存,如果命中则直接使用缓存中的资源不会再向服务器发送请求, 在chrome控制台的Network选项中可以看到该请求返回200的状态码,并且Size显示from disk cache或from memory cache。

这里的 header 中的信息指的是 expires 和 cahe-control.

Expires

该字段是 http1.0 时的规范,它的值为一个绝对时间的 GMT 格式的时间字符串,比如 Expires:Mon,18 Oct 2066 23:59:59 GMT。这个时间代表着这个资源的失效时间,在此时间之前,即命中缓存。这种方式有一个明显的缺点,由于失效时间是一个绝对时间,所以当服务器与客户端时间偏差较大时,就会导致缓存混乱。

  • 缓存过期时间,用来指定资源到期的时间,是服务器端的具体的时间点。

  • Expires=max-age + 请求时间,需要和Last-modified结合使用。

  • Expires是Web服务器响应消息头字段,在响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。

  • 该字段是 http1.0 时的规范,它的值为一个绝对时间的 GMT 格式的时间字符串,比如 Expires:Mon,18 Oct 2066 23:59:59 GMT。这个时间代表着这个资源的失效时间,在此时间之前,即命中缓存

  • 受限于本地时间,如果修改了本地时间,可能会造成服务器与客户端时间偏差较大时,就会导致缓存混乱。

Cache-Control

  • Cache-Control 是 http1.1 时出现的 header 信息,主要用于控制网页缓存。

  • 比如 Cache-Control:max-age=3600,代表着资源的有效期是 3600 秒(浏览器也会记录下来),这个时间内再次加载资源,就会命中强缓存。

  • Cache-Control 可以在请求头或者响应头中设置,并且可以组合使用多种指令: image.png

public:所有内容都将被缓存(客户端和代理服务器都可缓存)。具体来说响应可被任何中间节点缓存,如 Browser <-- proxy1 <-- proxy2 <-- Server,中间的proxy可以缓存资源。

private:所有内容只有客户端可以缓存,Cache-Control的默认取值。具体来说,表示中间节点不允许缓存,对于Browser <-- proxy1 <-- proxy2 <-- Server,proxy 会把Server 返回的数据发送给proxy1,自己不缓存任何数据,只做请求转发。

no-cache:客户端缓存内容需要经过协商缓存来验证决定是否使用。 表示不使用 Cache-Control的缓存控制方式做前置验证,而是使用 Etag 或者Last-Modified字段来控制缓存,浏览器在使用缓存数据时,需要先确认一下数据是否还跟服务器保持一致

no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存

max-age:max-age=xxx (xxx is numeric)表示缓存内容将在xxx秒后失效

s-maxage(单位为s):同max-age作用一样,只在代理服务器中生效(比如CDN缓存)。而s-maxage用于代理缓存。s-maxage的优先级高于max-age。如果存在s-maxage,则会覆盖掉max-age和Expires header。

max-stale:能容忍的最大过期时间。客户端可以接收一个已经过期了的响应。如果没有指定,接收任何age的响应(age表示响应由源站生成或确认的时间与当前时间的差值)。

min-fresh:能够容忍的最小新鲜度。客户端可以接受age加上min-fresh设定的时间之和的响应。

image.png

Cache-Control 与 Expires 可以在服务端配置同时启用,同时启用的时候 Cache-Control 优先级高

协商缓存

当强缓存没有命中的时候,浏览器会发送一个请求到服务器,服务器根据 header 中的部分信息来判断是否命中缓存。如果命中,则返回 304和Not Modified ,告诉浏览器资源未更新,可使用本地的缓存。

image.png

协商缓存失效,返回200和请求结果:

image.png

这里的 header 中的信息指的是 Last-Modify/If-Modify-Since 和 ETag/If-None-Match.

Last-Modify/If-Modify-Since

浏览器第一次请求一个资源的时候,服务器返回的 response header 中会加上 Last-Modify,Last-modify 是一个时间标识该资源的最后修改时间: Last-Modified: Fri, 22 Jul 2016 01:47:00 GMT

当浏览器再次请求该资源时,request 的请求头中会包含 If-Modify-Since,该值为缓存之前返回的 Last-Modify。服务器收到 If-Modify-Since 后,根据资源的最后修改时间判断是否命中缓存。

如果命中缓存,则返回 304,并且不会返回资源内容,并且不会返回 Last-Modify。If-Modified-Since的时间小于服务器中这个资源的最后修改时间,说明文件有更新,于是返回新的资源文件和200。

image.png

缺点:

Last-Modified 只能以秒计时,短时间内资源发生了改变,Last-Modified 并不会发生变化,服务端不能命中缓存导致发送相同的资源.

周期性变化。如果这个资源在一个周期内修改回原来的样子了,我们认为是可以使用缓存的,但是 Last-Modified 可不这样认为,因此便有了 ETag。

ETag/If-None-Match

  • Etag是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成),只要资源有变化,Etag就会重新生成。

  • 浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的Etag值放到request header里的If-None-Match里

  • 服务器只需要比较客户端传来的If-None-Match跟自己服务器上该资源的ETag是否一致,就能很好地判断资源相对客户端而言是否被修改过了。

  • 如果服务器发现ETag匹配不上,那么直接以常规GET 200回包形式新的资源(当然也包括了新的ETag)发给客户端;如果ETag是一致的,则直接返回304知会客户端直接使用本地缓存即可。

image.png

Last-Modified 与 ETag 是可以一起使用的,服务器会优先验证 ETag,一致的情况下,才会继续比对 Last-Modified,最后才决定是否返回 304

  • 首先在精确度上,Etag要优于Last-Modified。Last-Modified的时间单位是秒,如果某个文件在1秒内改变了多次,那么他们的Last-Modified其实并没有体现出来修改.

  • 但是Etag每次都会改变确保了精度;如果是负载均衡的服务器,各个服务器生成的Last-Modified也有可能不一致。

  • 第二在性能上,Etag要逊于Last-Modified,毕竟Last-Modified只需要记录时间,而Etag需要服务器通过算法来计算出一个hash值。

  • 第三在优先级上,服务器校验优先考虑Etag

总结

当浏览器再次访问一个已经访问过的资源时,它会这样做:

1.看看是否命中强缓存,如果命中,就直接使用缓存了。

2.如果没有命中强缓存,就发请求到服务器检查是否命中协商缓存。

3.如果命中协商缓存,服务器会返回 304 告诉浏览器使用本地缓存。

4.否则,返回200、最新的资源、缓存标识。

image.png

用户行为对浏览器缓存的影响

  • 打开网页,地址栏输入地址: 查找 disk cache 中是否有匹配。如有则使用;如没有则发送网络请求。

  • 普通刷新 (F5):因为 TAB 并没有关闭,因此 memory cache 是可用的,会被优先使用(如果匹配的话)。其次才是 disk cache。

  • 强制刷新 (Ctrl + F5):浏览器不使用缓存,因此发送的请求头部均带有 Cache-control:no-cache(为了兼容,还带了 Pragma:no-cache),服务器直接返回 200 和最新内容。

实践

强缓存 Cache-Control

image.png

index.js

const Koa = require('koa')
const path = require('path')
//静态资源中间件
const static = require('koa-static')

const app = new Koa()

// 静态资源目录对于相对入口文件index.js的路径
const staticPath = './static'



app.use( async ( ctx, next ) => {
   // 设置响应头Cache-Control 设置资源有效期为300秒
   ctx.set({
    'Cache-Control': 'max-age=300'  
  });
  await next();
})

app.use(static(
  path.join( __dirname,  staticPath)
))

app.listen(3000, () => {
  console.log('[demo] static-use-middleware is starting at port 3000')
})

刷新页响应头的Cache-Control变成了max-age=300。

验证三级缓存原理

进行网络请求后浏览器把图片存进了磁盘和内存中。 根据三级缓存原理,我们会先在内存中找资源,我们来刷新页面。可以发现是从内存缓存中查找返回。

image.png

关闭该页面,内存中的资源被释放掉, 但是磁盘中的资源是永久性的,所以还存在。 根据三级缓存原理,如果在内存中没找到资源,便会去磁盘中寻找!可以发现是从磁盘缓存中查找返回。

image.png

对资源设置的有效期是300秒,我们接下来来验证缓存是否失效。300秒后。。。缓存失效了。

image.png

验证协商缓存

index.js

const Koa = require('koa')
const path = require('path')
//静态资源中间件
const static = require('koa-static')

const  conditional = require('koa-conditional-get');
const  etag =  require('koa-etag');

const app = new Koa()

// 静态资源目录对于相对入口文件index.js的路径
const staticPath = './static'

app.use(conditional());
app.use(etag());

// app.use( async ( ctx, next ) => {
//    // 设置响应头Cache-Control 设置资源有效期为300秒
//    ctx.set({
//     'Cache-Control': 'max-age=300'  
//   });
//   await next();
// })

app.use(static(
 path.join( __dirname,  staticPath)
))

app.listen(3000, () => {
 console.log('[demo] static-use-middleware is starting at port 3000')
})


第一次请求.

image.png 我们发现返回值里面已经有了Etag值。

二次请求

浏览器带上If-None-Match请求头,并赋值为上一次返回头的Etag值,然后与 这次返回值的Etag值进行对比。如果一致则命中协商缓存。返回304 Not Modified。可以看到是命中的。

image.png

协商缓存失效

修改图片内容看看

image.png 首先我们二次刷新页面加载资源向服务器发送请求时,会将上一次返回的Etag值放到request header里的If-None-Match里。 Etag是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成),可以看到图片变化后,Etag重新生成了

现在可以明显看到 客户端浏览器请求头携带的If-None-Match跟服务器上该资源的ETag不一致,ETag匹配不上,以常规GET 200回包形式新的资源(当然也包括了新的ETag)发给客户端。

大功告成

github实践代码地址:github.com/LHDIYU/Koa/…