前端缓存知识汇总

前端缓存

  • 前端缓存主要是指缓存和浏览器缓存

  • 前端缓存可以加快页面加载速度、减轻服务器负担、减少延迟与网络阻塞、提高用户体验、支持离线使用等,能够有效的提升网站与应用的性能,但同时也面临着缓存过期、用户安全、缓存清除等问题

    1

缓存分类

缓存

缓存的优点

  • 减少了冗余的数据传递,节省宽带流量
  • 减少了服务器的负担,大大提高了网站性能
  • 加快了客户端加载网页的速度,这也正是HTTP缓存属于客户端缓存的原因

浏览器缓存策略

  • 默认情况下,浏览器都会使用缓存数据。

  • 用户在浏览器操作时,会触发怎样的缓存策略。主要有 3 种:

  • 浏览器地址栏输入url 回车: 查找 disk cache 中是否有匹配。如有则使用;如没有则发送网络请求。

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

  • 强制刷新 (Windows: Ctrl + F5。Mac: Cmd+Shift+R):会去服务器请求最新的资源文件下来。于是客户端就完成了强行更新的操作。

HTTP缓存

  • http缓存指的是:当客户端向服务器请求资源时,会先抵达浏览器缓存,如果浏览器有“要请求资源”的副本,就可以直接从浏览器缓存中提取而不是从原始服务器中提取这个资源
    • 常见的http缓存只能缓存get请求响应的资源,对于其他类型的响应则无能为力,所以后续说的请求缓存都是指GET请求。
      http缓存都是从第二次请求开始的。第一次请求资源时,服务器返回资源,并在respone header头中回传资源的缓存参数;
    • 第二次请求时,浏览器判断这些请求参数,命中强缓存就直接200,否则就把请求参数加到request header头中传给服务器,看是否命中协商缓存,命中则返回304,否则服务器会返回新的资源

HTTP缓存流程图

image-20240724145052233

HTTP缓存分类

  • 分为强缓存和协商缓存

    image-20240724144433167

强缓存
  • 浏览器在请求某一资源时,会先获取该资源缓存的header信息,判断是否命中强缓存(cache-control和expires信息),若命中直接从缓存中获取资源信息,包括缓存header信息
  • 本次请求根本就不会与服务器进行通信。状态码为200
Expires
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Expires

响应标头包含响应应被视为过期的日期/时间。

备注: 如果响应中有指令为 max-ages-maxageCache-Control 标头,则 Expires 标头会被忽略。max-age优先级更高

  • 是http1.0的规范,是一个响应头字段,表示资源的过期时间,它的值是一个绝对时间的GMT格式的时间字符串

  • 浏览器会将这个时间与客户端当前时间进行比较,判断资源是否过期。这个时间代表这这个资源的失效时间

  • 只要发送请求时间是在Expires之前,那么本地缓存始终有效,在此时间范围内,则从内存(或磁盘)中读取缓存返回

  • 比如说将某一资源设置响应头为:Expires:new Date(“2022-7-30 23:59:59”);那么,该资源在2022-7-30 23:59:59 之前,都会去本地的磁盘(或内存)中读取,不会去服务器请求。但是,Expires已经被废弃了。对于强缓存来说,Expires已经不是实现强缓存的首选

    缺点

    因为Expires判断强缓存是否过期的机制是:获取本地时间戳,并对先前拿到的资源文件中的Expires字段的时间做比较。来判断是否需要对服务器发起请求。这里有一个巨大的漏洞:“如果我本地时间不准咋办?”

    是的,Expires过度依赖本地时间,如果本地与服务器时间不同步,就会出现资源无法被缓存或者资源永远被缓存的情况。所以,Expires字段几乎不被使用了。现在的项目中,我们并不推荐使用Expires,强缓存功能通常使用cache-control字段来代替Expires字段。

Cache-Control

列举一些 Cache-control 字段常用的值:(完整的列表可以查看 MDN)

  • max-age:即最大有效时间,单位为秒数
  • must-revalidate:如果超过了 max-age 的时间,浏览器必须向服务器发送请求,验证资源是否还有效。
  • no-cache:虽然字面意思是“不要缓存”,但实际上还是要求客户端缓存内容的,只是是否使用这个内容由后续的对比来决定。
  • no-store: 真正意义上的“不要缓存”。所有内容都不走缓存,包括强制和对比。
  • public:所有的内容都可以被缓存 (包括客户端和代理服务器, 如 CDN)
  • private:所有的内容只有客户端才可以缓存,代理服务器不能缓存。默认值

这些值可以混合使用,例如 Cache-control:public, max-age=2592000。在混合使用时,它们的优先级如下图

image-20240724150520530

大部分情况,max-age=0no-cache 是等价的,但是还是要以具体浏览器为准

Cache-control 是一个相对时间,即使客户端时间发生改变,相对时间也不会随之改变,这样可以保持服务器和客户端的时间一致性。而且 Cache-control 的可配置性比较强大

Cache-control 的优先级高于 Expires,为了兼容 HTTP/1.0 和 HTTP/1.1,实际项目中两个字段我们都会设置

协商缓存

也叫对比缓存

Last-Modified
  • Last-Modified代表资源的最后修改时间,其属于响应首部字段。当浏览器第一次接收到服务器返回资源的 Last-Modified 值后,其会把这个值存储起来

image-20240724152124888

If-Modified-Since
Last-Modified: Fri , 14 May 2021 17:23:13 GMT 
If-Modified-Since: Fri , 14 May 2021 17:23:13 GMT
  • 下次访问该资源时通过携带If-Modified-Since请求首部发送给服务器验证该资源是否过期
  • 那么之后每次对该资源的请求,都会带上If-Modified-Since这个字段,而务端就需要拿到这个时间并再次读取该资源的修改时间,让他们两个做一个比对来决定是读取缓存还是返回新的资源。
  • 如果在If-Modified-Since字段指定的时间之后资源都没有发生更新,那么服务器会返回状态码 304 Not Modified 的响应

image-20240724152304503

image-20240724152625925

image-20240724152719640

缺陷
  • 因为是更具文件修改时间来判断的,所以,在文件内容本身不修改的情况下,依然有可能更新文件修改时间(比如修改文件名再改回来),这样,就有可能文件内容明明没有修改,但是缓存依然失效了。
  • 当文件在极短时间内完成修改的时候(比如几百毫秒)。因为文件修改时间记录的最小单位是秒,所以,如果文件在几百毫秒内完成修改的话,文件修改时间不会改变,这样,即使文件内容修改了,依然不会 返回新的文件
  • 如果文件是通过服务器动态生成的,那么该方法的更新时间永远是生成的时间,尽管文件可能没有变化,所以起不到缓存的作用

为了解决上述的缺陷问题。从http1.1开始新增了一个头信息,ETag(Entity 实体标签)

Etag

比较前后缓存的文件指纹

文件指纹:根据文件内容计算出的唯一哈希值。文件内容一旦改变则指纹改变。

  • 第一次请求某资源的时候,服务端读取文件并计算出文件指纹,将文件指纹放在响应头的etag字段中跟资源一起返回给客户端
if-None-Match
  • 第二次请求某资源的时候,客户端自动从缓存中读取出上一次服务端返回的ETag也就是文件指纹。并赋给请求头的if-None-Match字段,让上一次的文件指纹跟随请求一起回到服务端
  • 服务端拿到请求头中的is-None-Match字段值(也就是上一次的文件指纹),并再次读取目标资源并生成文件指纹,两个指纹做对比。
    • 如果两个文件指纹完全吻合,说明文件没有被改变,则直接返回304状态码和一个空的响应体并return。
    • 如果两个文件指纹不吻合,则说明文件被更改,那么将新的文件指纹重新存储到响应头的ETag中并返回给客户端

image-20240724154723157

image-20240724154832842

Etag 的优先级高于 Last-Modified

精确度上,Etag 优于Last-Modified

性能上,Last-Modified优于Etag

缺陷
  • ETag需要计算文件指纹这样意味着,服务端需要更多的计算开销。。如果文件尺寸大,数量多,并且计算频繁,那么ETag的计算就会影响服务器的性能。显然,ETag在这样的场景下就不是很适合。
  • ETag有强验证和弱验证
    • 所谓将强验证,ETag生成的哈希码深入到每个字节。哪怕文件中只有一个字节改变了,也会生成不同的哈希值,它可以保证文件内容绝对的不变。但是,强验证非常消耗计算量。
    • ETag还有一个弱验证,弱验证是提取文件的部分属性来生成哈希值。因为不必精确到每个字节,所以他的整体速度会比强验证快,但是准确率不高。会降低协商缓存的有效性。

缓存机制

强制缓存优先于协商缓存进行

若强制缓存(Expires和Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since和Etag / If-None-Match)

协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,返回200,重新返回资源和缓存标识,再存入浏览器缓存中;

生效则返回304,继续使用缓存

image-20240724162004480

浏览器缓存

Service Worker Cache

一种在浏览器后台运行的JavaScript脚本,它可以拦截和处理网页发出的网络请求,以及管理缓存和离线数据。Service Worker可以让网页在离线状态下仍能正常访问,并且可以提高网页的性能和响应速度。它由开发者编写的额外的脚本控制,且缓存位置独立。

Service Worker可以实现以下功能:

  • 离线缓存:Service Worker可以缓存网页资源,使得用户在离线状态下仍能访问已经缓存的资源。
  • 推送通知:Service Worker可以接收来自服务器的推送通知,并在用户离线时显示通知。
  • 资源拦截:Service Worker可以拦截网页发出的网络请求,并根据需要返回缓存中的资源或者从服务器请求最新的资源。
  • 跨域通信:Service Worker可以和其他域名的网页进行通信,从而实现一些跨域的功能。

如果 Service Worker 没能命中缓存,一般情况会使用 fetch() 方法继续获取资源。这时浏览器就去 memory cache 或者 disk cache 进行下一次找缓存的工作了。经过 Service Worker 的 fetch() 方法获取的资源,即便它并没有命中 Service Worker 缓存,甚至实际走了网络请求,也会标注为 from ServiceWorker。

注意:为了保证安全性,Service Worker只能在HTTPS协议下使用。

Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的

  • Service Worker 实现缓存功能一般分为三个步骤
    • 首先需要先注册 Service Worker
    • 然后监听到 install 事件以后就可以缓存需要的文件
    • 那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据

Memory Cache

  • memory cache(内存缓存)是存储在浏览器内存中的。
  • 其优点为获取速度快、优先级高,从内存中获取资源耗时为 0 ms
  • 而其缺点也显而易见,比如生命周期短,当网页关闭后内存就会释放,同时虽然内存非常高效,但它也受限制于计算机内存的大小,是有限的。那么如果要存储大量的资源,这是还得用到磁盘缓存。
  • 读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。
  • 一旦我们关闭 Tab 页面,内存中的缓存也就被释放了

当我们访问过页面以后,再次刷新页面,可以发现很多数据都来自于内存缓存

image-20240724160828932

内存缓存中有一块重要的缓存资源是preloader相关指令(例如<link rel="prefetch">)下载的资源。总所周知preloader的相关指令已经是页面优化的常见手段之一,它可以一边解析js/css文件,一边网络请求下一个资源。

需要注意的事情是,内存缓存在缓存资源时并不关心返回资源的HTTP缓存头Cache-Control是什么值,同时资源的匹配也并非仅仅是对URL做匹配,还可能会对Content-Type,CORS等其他特征做校验

Disk Cache

  • disk cache(硬盘缓存)是存储在计算机硬盘中的。
  • 其优缺点与 memory cache 正好相反,比如优点是生命周期长,不触发删除操作则一直存在,
  • 而缺点则是获取资源的速度相对内存缓存较慢。
  • disk cache 会根据保存下来的资源的 HTTP 首部字段来判断它们是否需要重新请求,如果重新请求那便是强缓存的失效流程,否则便是生效流程。
  • 获取顺序:
    • 浏览器会率先查找内存缓存,如果资源在内存中存在,那么直接从内存中加载
    • 如果内存中没找到,接下去会去磁盘中查找,找到便从磁盘中获取
    • 如果磁盘中也没有找到,那么就进行网络请求,并将请求后符合条件的资源存入内存和磁盘中

Push Cache

Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在Chrome浏览器中只有5分钟左右,同时它也并非严格执行HTTP头中的缓存指令

Push Cache 在国内能够查到的资料很少,也是因为 HTTP/2 在国内不够普及。这里推荐阅读Jake ArchibaldHTTP/2 push is tougher than I thought 这篇文章,文章中的几个结论:

  • 所有的资源都能被推送,并且能够被缓存,但是 Edge 和 Safari 浏览器支持相对比较差
  • 可以推送 no-cache 和 no-store 的资源
  • 一旦连接被关闭,Push Cache 就被释放
  • 多个页面可以使用同一个HTTP/2的连接,也就可以使用同一个Push Cache。这主要还是依赖浏览器的实现而定,出于对性能的考虑,有的浏览器会对相同域名但不同的tab标签使用同一个HTTP连接。
  • Push Cache 中的缓存只能被使用一次
  • 浏览器可以拒绝接受已经存在的资源推送
  • 你可以给其他域名推送资源

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

存储型缓存

浏览器存储型缓存包含了 Cookie、Web Storage、IndexedDB、WebSQL 等

Cookie

  • Cookie 的存储空间很小,不能超过 4KB,因此这一缺点也限制了它用于存储较大容量数据的能力
  • Cookie 在同域下会伴随着每一次资源请求的请求报头传递到服务端进行验证,试想一下如果大量非必要的数据存储在 Cookie 中,伴随着请求响应会造成多大的无效资源传输及性能浪费

Web Storage

Web Storage 即为 Session Storage 和 Local Storage

  • Session Storage 作为临时性的本地存储,其生命周期存在于网页会话期间,即使用 Session Storage 存储的缓存数据在网页关闭后会自动释放,并不是持久性的。

  • Local Storage 则存储于浏览器本地,除非手动删除或过期,否则其一直存在,属于持久性缓存。

  • 般为 2.5-10M 之间(各家浏览器不同),这容量对于用于网页数据存储来说已经十分充足

  • 存储对象时,需要JSON.stringify 转化为字符串对象

IndexedDB和WebSQL

  • IndexedDB 和 WebSQL 都是一种低级API,用于客户端存储大量结构化数据。
  • 该API使用索引来实现对该数据的高性能搜索。
  • 不同的是IndexedDB是非关系型,而WebSQL是关系型。
  • WebSQL官方不在维护,但兼容性较好
  • IndexedDB在维护,兼容性较差

三类缓存优先级

  • Service Worker: 缓存由于其可以完全控制网络请求,因此具有最高的优先级,即使是强制缓存也可以被它所覆盖。
  • 强制缓存: 如果存在强制缓存,并且缓存没有过期,则直接使用强制缓存,不需要向服务器发送请求。
  • 协商缓存:如果强制缓存未命中,但协商缓存可用,则会向服务器发送条件请求,询问资源是否更新。如果服务器返回 304 Not Modified 响应,则直接使用缓存。
  • Web Storage 缓存:Web Storage 缓存的优先级最低,只有在网络不可用或者其他缓存都未命中时才会生效。

前端缓存的性能优化策略

缓存的应用模式

不常变化的资源

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

在线提供的类库 (如 jquery-3.3.1.min.js, lodash.min.js 等) 均采用这个模式。如果配置中还增加 public 的话,CDN 也可以缓存起来,效果拔群。

这个模式的一个变体是在引用 URL 后面添加参数 (例如 ?v=xxx 或者 ?_=xxx),这样就不必在文件名或者路径中包含动态参数,满足某些完美主义者的喜好。在项目每次构建时,更新额外的参数 (例如设置为构建时的当前时间),则能保证每次构建后总能让浏览器请求最新的内容

经常变化的资源

这里的资源不单单指静态资源,也可能是网页资源,例如博客文章。这类资源的特点是:URL 不能变化,但内容可以(且经常)变化。我们可以设置 Cache-Control: no-cache 来迫使浏览器每次请求都必须找服务器验证资源是否有效。

非常危险的模式 1 和 2 的结合

设置了 no-cache,相当于 max-age=0, must-revalidate。我的应用时效性没有那么强,但又不想做过于长久的强制缓存,我能不能配置例如 max-age=600, must-revalidate 这样折中的设置呢?

表面上看这很美好:资源可以缓存 10 分钟,10 分钟内读取缓存,10 分钟后和服务器进行一次验证,集两种模式之大成,但实际线上暗存风险。因为上面提过,浏览器的缓存有自动清理机制,开发者并不能控制

  • 频繁变动的资源,比如HTML,采用协商缓存

  • CSS、JS、图片等资源采用强缓存,避开启发式缓存,使用 content hash 命名

  • 缓存API响应,在相同请求再次发生时,可以直接从缓存中获取结果,而无需重新查询API

  • 采用效率更高的br压缩算法(Brotli),可以减小传输文件的大小,从而缩短加载时间

    • 参考文章:https://zhuanlan.zhihu.com/p/667939696

    • gzip(GNU zip): 使用DEFLATE算法,是Web服务器中最常见的压缩算法,适用于文本文件,如HTML、CSS、JavaScript等。

    • deflate: 也使用DEFLATE算法,但与gzip不同的是,它没有使用gzip的文件头和尾,使得数据包更小,由于与一些旧的客户端和服务器不兼容所以不太常见。

    • br(Brotli): Brotli是由Google开发的一种新的压缩算法,通常提供比gzip更好的压缩比,在支持Brotli的情况下,应该使用它作为首选的压缩算法。

    • compress: 是一种基于 Lempel-Ziv-Welch (LZW) 算法的压缩方法。这种算法在 HTTP 数据压缩机制中曾经被广泛使用,但由于专利问题,现在已经相对较少见。

  • 合并请求,如果把多个访问小文件的请求合并成一个大的请求,虽然传输的总资源还是一样,但是减少请求,也就意味着减少了重复发送的 HTTP 头部

  • 延迟加载,对于不需要立即显示的资源(如图片或视频),可以使用延迟加载技术。这样,只有当用户滚动到这些资源时,它们才会开始加载

  • 善用preload和prefetch,我们可以优化浏览器资源加载的顺序和时机

  • 善用base64,有效命中memory cache

  • 使用HTTP/2,HTTP/2协议提供了性能改进,如多路复用和服务器推送。启用HTTP/2可以进一步提高网站性能

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

所谓浏览器的行为,指的就是用户在浏览器如何操作时,会触发怎样的缓存策略。主要有 3 种:

  • 打开网页,地址栏输入地址: 查找 disk cache 中是否有匹配。如有则使用;如没有则发送网络请求。
  • 普通刷新 (F5):因为 TAB 并没有关闭,因此 memory cache 是可用的,会被优先使用(如果匹配的话)。其次才是 disk cache。
  • 强制刷新 (Ctrl + F5):浏览器不使用缓存,因此发送的请求头部均带有 Cache-control: no-cache(为了兼容,还带了 Pragma: no-cache)。服务器直接返回 200 和最新内容。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值