缓存机制

116 阅读17分钟
  • 缓存概念

    浏览器缓存是浏览器对之前请求过的文件进行缓存,以便下一次访问时时重复使用,节省带宽,提高访问速度,降低服务器压力

image.png

  • 强缓存 浏览器不会向服务器发送任何请求,直接从本地缓存中读取文件,并返回200状态码

(1)不存在该缓存结果和缓存标识,强制缓存失效,则直接向服务器发起请求(跟第一次发起请求一致),如下图:

(2)存在该缓存结果和缓存标识,但是结果已经失效,强制缓存失效,则使用协商缓存(暂不分析),如下图

(3)存在该缓存结果和缓存标识,且该结果没有还没有失效,强制缓存生效,直接返回该结果,如下图:

\

  • 协商缓存 向服务器发送请求,服务器会根据这个请求的request header的一些参数来判断是否命中协商缓存,如果命中,则返回304状态码并带上新的response header通知浏览器从缓存中读取资源

强缓存

  • 缓存相关header

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

    • Cache-Control 请求/响应头 缓存控制字段 精确控制缓存策略

      (1) max-age:用来设置资源(representations)可以被缓存多长时间,单位为秒;
      (2) s-maxage:和max-age是一样的,不过它只针对代理服务器缓存而言;
      (3)public:指示响应可被任何缓存区缓存;
      (4)private:只能针对个人用户,而不能被代理服务器缓存;
      (5)no-cache:强制客户端直接向服务器发送请求,也就是说每次请求都必须向服务器发送。服务器接收 到 请求,然后判断资源是否变更,是则返回新内容,否则返回304,未变更。这个很容易让人产生误解,使人误 以为是响应不被缓存。实际上Cache-Control: no-cache是会被缓存的,只不过每次在向客户端(浏览器)提供响应数据时,缓存都要向服务器评估缓存响应的有效性。
      (6)no-store:禁止一切缓存(这个才是响应不被缓存的意思)。

image.png 状态码为灰色的请求则代表使用了强制缓存,请求对应的Size值则代表该缓存存放的位置,分别为from memory cache 和 from disk cache

那么from memory cache 和 from disk cache又分别代表的是什么呢?什么时候会使用from disk cache,什么时候会使用from memory cache呢?

from memory cache代表使用内存中的缓存,from disk cache则代表使用的是硬盘中的缓存,浏览器读取缓存的顺序为memory –> disk。

访问heyingye.github.io/ –> 200 –> 关闭博客的标签页 –> 重新打开heyingye.github.io/ –> 200(from disk cache) –> 刷新 –> 200(from memory cache)

内存缓存(from memory cache) :内存缓存具有两个特点,分别是快速读取时效性

1、快速读取:内存缓存会将编译解析后的文件,直接存入该进程的内存中,占据该进程一定的内存资源,以方便下次运行使用时的快速读取。

2、时效性:一旦该进程关闭,则该进程的内存则会清空。

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

 

在浏览器中,浏览器会在js和图片等文件解析执行后直接存入内存缓存中,那么当刷新页面时只需直接从内存缓存中读取(from memory cache);而css文件则会存入硬盘文件中,所以每次渲染页面都需要从硬盘读取缓存(from disk cache)。

协商缓存

-   If-Modified-Since 请求头,资源最近修改时间,由浏览器告诉服务器

-   Last-Modified 响应头,资源最近修改时间,由服务器告诉浏览器

-   Etag 响应头,资源标识,由服务器告诉浏览器

-   If-None-Match 请求头,缓存资源标识,由浏览器告诉服务器
  • 配对使用的字段

    • If-Modified-Since 和 Last-Modified
    • Etag 和 If-None-Match
  • 服务器和浏览器约定资源过期时间

    • 用Expires字段来控制,时间是GMT格式的标准时间,如 Fri, 01 Jan 1990 00:00:00 GMT。
    • 缺点:缓存过期以后,服务器不管资源有无变化都会再次读取资源并返回给浏览器
  • 服务器告诉浏览器资源上次修改时间

    • 服务器每次返回资源的时候,还要告诉浏览器资源在服务器最近修改的时间 Last-Modified (GMT标准格式)

    • 相比上一个方案的优点

      • 缓存过期后,服务器检测如果资源没有变化,就不再重新发送资源
      • 缓存过期后,服务器检测如果资源有变化,就重新发送资源
    • 缺点

      • Expires过期控制不稳定,因为浏览器可以任意修改时间,导致浏览器和服务器的时间有误差
      • last-modified 过期时间只能精确到秒
  • 增加相对时间控制

    • 服务器除了告诉浏览器Expires,同时告诉浏览器一个相对时间cache-control:max-age=10秒,意思是10秒以内,使用缓存到浏览器的资源
    • 优先级 cache-control大于expires
  • 增加文件内容对比,引入Etag

    • 为了解决文件修改时间只能精确到秒的问题。当文件的内容变了,Etag才变,相当于Etag是文件内容的唯一标识,同时引入对应的请求头if-none-match,每次浏览器请求服务器的时候,都带上if-none-match字段,该字段的值就是上次请求该文件时,服务器返回给浏览器的Etag
  • 结束了吗

到此就结束了吗? 是的,http的缓存机制就是如此了,但是仍然存在一个问题:

浏览器无法主动得知服务器上的 a.js 资源变化了。

不管用 Expires 还是 Cache-Control,他们都只能够控制缓存是否过期,但是在缓存过期之前,浏览器是无法得知服务器上的资源是否变化的。只有当缓存过期后,浏览器才会发请求询问服务器。

  • 最终方案

大家可以想象我们使用 a.js 的场景,我们一般都是输入网址,访问一个 html 文件,html文件中会引入 js、css 、图片等资源。

所以呢,我们在html上做些手脚。

我们不让 html 文件缓存,每次访问 html 都去请求服务器。所以浏览器每次都能拿到最新的html资源。

a.js 内容更新的时候,我们修改一下 html 中 a.js 的版本号。

第一次访问 html

<script src="http://test.com/a.js?version=0.0.1"></script>

浏览器下载0.0.1版本的a.js文件。

浏览器再次访问 html,发现还是0.0.1版本的a.js文件,则使用本地缓存。

某一天a.js变了,我们的html文件也相应变化如下:

<script src="http://test.com/a.js?version=0.0.2"></script>

浏览器再次访问html,发现【test.com/a.js?versio… a.js。

如此往复。

所以,通过设置html不缓存,html引用资源内容变化则改变资源路径的方式,就解决了无法及时得知资源更新的问题。

当然除了以版本号来区分,也可以以 MD5hash 值来区分。 如

<script src="http://test.com/a.【hash值】.js"></script>

使用webpack打包的话,借助插件可以很方便的处理。

启发式缓存

就是响应中没有ExpiresCache-Control:max-ageCache-Control:s-maxage,并且响应中不包含其他有关缓存的限制,缓存可以使用启发式方法计算缓存有效期

通常会根据响应头中的Date字段(报文创建时间)减去Last-Modified值的10%作为缓存时间

max(0,(Date - Last-Modified)) % 10
复制代码

缓存实际使用策略

对于频繁变动的资源

使用Cache-Control:no-cache,使浏览器每次都请求数据,然后配合EtagLast-Modified来验证资源是否有效,这样虽然不能节省请求数量,但能显著减少响应数据大小

对于不常变化的资源

可以给它们的Cache-Control配置一个很大的max-age=31536000(一年),这样浏览器之后请求相同的URL会命中强缓存,而为了解决更新问题,就需要在文件名(或者路径)中添加hash,版本号等动态字符,之后更改动态字符,从而达到更改引用URL的目的,让之前的强缓存失效(其实并未立即失效,只是不再使用了而已)

缓存存放位置,和读取的优先级

优先级就是按下面顺序

1. Service Worker

可以查看我另一篇文章有详细介绍

2. Memory Cache(内存)

就是将资源存储在内存中,下次访问直接从内存中读取。例如刷新页面时,很多数据都是来自于内存缓存。一般存储脚本、字体、图片。

优点是读取速度快;缺点由于一旦关闭Tab标签页,内存中的缓存也就释放了,所以容量和存储时效上差些

3. Disk Cache(硬盘)

就是将资源存储在硬盘中,下次访问时直接从硬盘中读取。它会根据请求头中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使是跨域站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次请求。

优点是缓存在硬盘中,容量大,并且存储时效性更长;缺点是读取速度慢些

4. Push Cache

这个是推送缓存,是HTTP/2中的内容,当上面三种缓存都没有命中时才会,被使用。它只会存在于Session中,一旦会话结束就会释放,所以缓存时间很短,而且Push Cache中的缓存只能被使用一次

总结

强制缓存优先于协商缓存进行,若强制缓存(Expires和Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since和Etag / If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,重新获取请求结果,再存入浏览器缓存中;生效则返回304,继续使用缓存

其它缓存

DNS缓存

进入页面的时候会进行DNS查询,找到域名对应的服务器的IP地址,再发送请求

网上流程图很多,我从中借鉴了两张

DNS域名查找先在客户端进行递归查询,如图

在任何一步找到就会结束查找流程,而整个过程客户端只发出一次查询请求

如果都没有找到,就会走DNS服务器设置的转发器,如果没设置转发模式,则向13根发起解析请求,这里就是迭代查询,如图

13根:
全球共有13个根域服务器IP地址,不是13台服务器!
因为借助任播技术,可以在全球设立这些IP的镜像站点,所以访问的不是唯一的那台主机
复制代码

很明显,整个过程会发出多次查询请求

在第一次进入页面后就会把DNS解析的地址记录缓存在客户端,之后再进的话至少不需要发起后面的迭代查询了,从而速度更快

CDN缓存

当我们发送一个请求时,浏览器本地缓存失效的情况下,CDN会帮我们去计算哪得到这些内容的路径短而且快。

比如在广州请求广州的服务器就比请求新疆的服务器响应速度快得多,然后向最近的CDN节点请求数据

CDN会判断缓存数据是否过期,如果没有过期,则直接将缓存数据返回给客户端,从而加快了响应速度。如果CDN判断缓存过期,就会向服务器发出回源请求,从服务器拉取最新数据,更新本地缓存,并将最新数据返回给客户端。

CDN不仅解决了跨运营商和跨地域访问的问题,大大降低访问延时的同时,还起到了分流的作用,减轻了源服务器的负载

进阶知识体系之你不能不知道的CDN

几种刷新和回车的区别

  • 使用 Ctrl+F5 强制刷新页面时,会对本地缓存文件直接过期,然后跳过强缓存和协商缓存,直接请求服务器
  • 点击刷新或 F5 刷新页面时,对本地缓存文件过期,然后带If-Modifed-SinceIf-None-Match发起协商缓存验证新鲜度
  • 浏览器输入URL回车,浏览器查找 Disk Cache,有则使用,没有则发送网络请求

本地存储

Cookie

最早被提出来的本地存储方式,在每一次 http 请求携带 Cookie,可以判断多个请求是不是同一个用户发起的,特点是:

  • 有安全问题,如果被拦截,就可以获得 Session 所有信息,然后将 Cookie 转发就能达到目的。(关于攻击和防范本可以看我另一篇文章 吃透浏览器安全(同源限制/XSS/CSRF/中间人攻击))
  • 每个域名下的Cookie不能超过20个,大小不能超过4kb
  • Cookie在请求新页面的时候都会被发送过去
  • Cookie创建成功名称就不能修改
  • 跨域名不能共享Cookie

如果要跨域名共享Cookie有两个方法

  • 用 Nginx 反向代理
  • 在一个站点登录之后,往其他网站写 Cookie。服务端的 Session 存储到一个节点,Cookie 存储 SessionId

Cookie的使用场景

  • 最常见的就是 Cookie 和 Session 结合使用,将 SessionId 存储到 Cookie 中,每次请求都会带上这个 SessionId 这样服务端就知道是谁发起的请求
  • 可以用来统计页面的点击次数

Cookie都有哪些字段

  • NameSize 故名思意
  • Value:保存用户登录状态,应该将该值加密,不能使用明文
  • Path:可以访问此 Cookie 的路径。比如 juejin.cn/editor ,path是/editor,只有/editor这个路径下的才可以读取 Cookie
  • httpOnly:表示禁止通过 JS 访问 Cookie,减少 XSS 攻击。
  • Secure:只能在 https 请求中携带
  • SameSite:规定浏览器不能在跨域请求中携带 Cookie 减少 CSRF 攻击,详细说明看这里
  • Domain:域名,跨域或者 Cookie 的白名单,允许一个子域获取或操作父域的 Cookie,实现单点登录的话会非常有用
  • Expires/Max-size:指定时间或秒数的过期时间,没设置的话就和 Session 一样关闭浏览器就失效

LocaStorage

是H5的新特性,是将信息存储到本地,它的存储大小比 Cookie 大得多,有5M,而且是永久存储,除非主动清理,不然会一直存在

受到同源策略限制,就是端口、协议、主机地址有任何一样不同都不能访问,还有在浏览器设为隐私模式下,也不能读取 LocalStorage

它的使用场景就很多了,比如存储网站主题、存储用户信息、等等,存数数据量多或者不怎么改变的数据都可以用它

SessionStorage

SessionStorage 也是H5新特性,主要用于临时保存同一窗口或标签页的数据,刷新页面时不会删除,但是关闭窗口或标签页之后就会删除这些数据

SessionStorage 和 LocalStorage 一样是在本地存储,而且都不能被爬虫爬取,并且都有同源策略的限制,只不过 SessionStorage 更加严格,只有在同一浏览器的同一窗口下才能共享

它的 API 和 LocalStorage 也一样 getItem、setItem、removeItem、clear、key

它的使用场景一般是具有时效性的,比如存储一些网站的游客登录信息,还有临时的浏览记录等

indexDB

是浏览器本地数据库,有以下特点

  • 键值对储存:内部用对象仓库存放数据,所有类型的数据都可以直接存入,包括js对象,以键值对的形式保存,每条数据都有对应的主键,主键是唯一的
  • 异步:indexDB操作时用户依然可能进行其他操作,异步设计是为了防止大量数据的读写,拖慢网页的表现
  • 支持事务:比如说修改整个表的数据,修改了一半的时候报了个错,这时候会全部恢复到没修改之关的状态,不存在修改一半成功的情况
  • 同源限制:每一个数据库应创建它对应的域名,网页只能访问自身域名下的数据库
  • 存储空间大:一般来说不少于250MB,甚至没有上限
  • 支持二进制存储:比如ArrayBuffer对象和Blob对象

前端存储方式除了上面四个,还有WebSQL,类似于SQLite,是真正意义上的关系型数据库,可以使用sql进行操作,只是用js时要进行转换,比较麻烦

上面四个的区别

CookieSessionStorageLocalStorageindexDB
存储大小4k5M或更大5M或更大无限
存储时间可指定时间,没指定关闭窗口就失效浏览器窗口关闭就失效永久有效永久有效
作用域同浏览器,所有同源标签页当前标签页同浏览器,所有同源标签页
存在于请求中来回传递客户端本地客户端本地客户端本地
同源策略同浏览器,只能被同源同路径页面访问共享自己用同浏览器,只能被同源页面访问共享

离线存储

Service Worker

Service Worker是运行js主线程之外的,在浏览器背后的独立线程,自然也无法访问DOM,它相当于一个代理服务器,可以拦截用户发出的请求,修改请求或者直接向用户发出回应,不用联系服务器。比如加载JS和图片,这就让我们可以在离线的情况下使用网络应用

一般用于离线缓存(提高首屏加载速度)、消息推送网络代理等功能。使用Service Worker的话必须使用https协议,因为Service Worker中涉及到请求拦截,需要https保障安全

用Service Worker来实现缓存分三步:

  • 一是注册
  • 然后监听install事件后就可以缓存文件
  • 下次再访问的时候就可以通过拦截请求的方式直接返回缓存的数据
// index.js 注册
if (navigator.serviceWorker) { 
    navigator.serviceWorker .register('sw.js').then( registration => {
        console.log('service worker 注册成功')
    }).catch((err)=>{
        console.log('servcie worker 注册失败')
    })
} 
// sw.js  监听 `install` 事件,回调中缓存所需文件 
self.addEventListener('install', e => {
    // 打开指定的缓存文件名
    e.waitUntil(caches.open('my-cache').then( cache => {
        // 添加需要缓存的文件
        return cache.addAll(['./index.html', './index.css'])
    }))
})
// 拦截所有请求事件 缓存中有请求的数据就直接用缓存,否则去请求数据 
self.addEventListener('fetch', e => { 
    // 查找request中被缓存命中的response
    e.respondWith(caches.match(e.request).then( response => {
        if (response) {
            return response
        }
        console.log('fetch source')
    }))
})