前端缓存看这一篇就够了
前端缓存看这一篇就够了
缓存是什么
缓存是一种保存资源副本并在下次请求时直接使用该副本的技术。
为什么要缓存(缓存的优点)
-
缓解服务器压力(不用每次去请求资源)
-
提升性能,提高访问速度(打开本地资源速度当然比请求回来再打开要快得多)
-
减少网络 IO 消耗,减少带宽消耗
-
通过网络获取内容既速度缓慢又开销巨大
-
如果是较大的响应需要在客户端与服务器之间进行多次往返通信,这会延迟浏览器获得和处理内容的时间,还会增加访问者的流量费用
缓存类型
缓存总体可分为:私有缓存(private)与共享缓存(public)
-
私有缓存:该资源只能被浏览器缓存,private 为默认值,只能用于单独用户
-
共享缓存:该资源既可以被浏览器缓存,也可以被代理服务器缓存,能够被多个用户使用
按种类分的话可以分为:
-
数据库缓存
-
代理服务器缓存
-
网关缓存(CDN缓存)
-
浏览器缓存
这里我们主要聊一下浏览器缓存
浏览器缓存机制
我们从三个方面去了解浏览器的缓存机制:
-
浏览器缓存位置分析
-
HTTP缓存策略(主要)
-
用户行为对缓存的影响
浏览器缓存位置分析
从缓存位置上来说分为四种,并且各自有优先级,当依次查找缓存且都没有命中的时候,才会去请求网络:
-
Service Worker
-
Memory Cache
-
Disk Cache
-
Push Cache
Service Worker
-
Service Worker 是一种独立于主线程之外的 Javascript 线程。它脱离于浏览器窗体,可以帮我们实现离线缓存、消息推送和网络代理等功能。
-
使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。
-
Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们*控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。
-
Service workers利用了ES6中比较重要的特性Promise,并且在拦截请求的时候使用的是新的fetch API,之所以使用fetch就是因为fetch返回的是Promise对象。
-
Service Worker 实现缓存功能一般分为三个步骤:首先需要先注册 Service Worker,然后监听到 install 事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。
Memory Cache
-
MemoryCache,是指存在内存中的缓存。包括当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。
-
因为存储在内存中,MemoryCache 是响应速度最快的一种缓存,但由于同样的原因,缓存持续性很短,会随着进程的释放而释放。 一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。
-
MemoryCache那么的高效,是否所有资源都可以放进内存缓存中?那肯定是不行的。
-
计算机内存有限,比硬盘容量小很多,浏览器会考虑计算机具体情况来决定缓存放在内存中还是硬盘中。
-
内存缓存在缓存资源时并不关心返回资源的HTTP缓存头信息,小文件优先缓存在内存中
Disk Cache
-
Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。
-
会根据 HTTP Herder 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求
对于到底哪些文件会缓存进内存,哪些进硬盘,参考如下:
-
对于大文件来说,大概率是不存储在内存中的,反之优先
-
当前系统内存使用率高的话,文件优先存储进硬盘
从图中我们可以看到:
-
Base64 格式的图片,几乎永远可以被塞进 memory cache,加载时间为0,都是直接加载好的。
-
较大的样式文件和js文件都会进 Disk Cache,加载时间也相对较长
有时候我们还会看到如下图:
prefetch cache是一种浏览器预加载机制,其利用浏览器空闲时间来下载或预取用户在不久的将来可能访问的文档。网页向浏览器提供一组预取提示,并在浏览器完成当前页面的加载后开始静默地拉取指定的文档并将其存储在缓存中。当用户访问其中一个预取文档时,便可以快速的从浏览器缓存中得到。
如何实现:对要预加载的文件的 link 标签加上 rel=“prefetch”
<link rel="prefetch" href="/images/big.jpeg">
Push Cache
-
Push Cache 是 HTTP2 在 server push 阶段存在的缓存,当以上三种缓存都没有命中时,它才会被使用。
-
Push Cache 是一种存在于会话阶段的缓存,当 session 终止时,缓存也随之释放。
-
不同的页面只要共享了同一个 HTTP2 连接,那么它们就可以共享同一个 Push Cache。
-
Push Cache 中的缓存只能被使用一次
-
可以推送 no-cache 和 no-store 的资源
-
浏览器可以拒绝接受已经存在的资源推送
HTTP 缓存策略(主要)
当客户端向服务器请求资源时,会先抵达浏览器缓存,当以上缓存都没有命中的时候,我们就需要发起请求获取资源了。当然为了性能考虑,我们肯定不希望所有的资源每次都是请求来的。所以需要给不同的资源选择不同的缓存策略。
然而常见的 HTTP 缓存只能存储 GET 响应,对于其他类型的响应则无能为力。
通常浏览器缓存策略分为三种:强缓存,协商缓存和启发式缓存,并且缓存策略都是通过设置 HTTP Header 来实现的。
强缓存
-
不会向服务器发送请求,直接从缓存中读取资源。
-
状态码:200,显示 from disk cache 或 from memory cache。
-
设置两种 HTTP Header 实现:Expires 和 Cache-Control。
从 expires 到 cache-control
expires
要实现强缓存,在 HTTP1.0 的时代,我们用 expires 来实现。
expires 的值是一个时间戳,表示本地时间到这个设置的时间缓存就失效。这样一来 expires 就是有问题的,受限于本地时间,如果服务端和客户端的时间设置可能不同,或者我直接手动去把客户端的时间改掉,那么 expires 将无法达到我们的预期。
因为 expires 的缺陷,HTTP1.1 新增了 Cache-Control 字段来完成 expires 的任务,Cache-Control 优先级高于 expires。继续使用 expires 的唯一目的就是向下兼容。
cache-control
通过 max-age 来控制资源的有效期。max-age 不是一个时间戳,而是一个时间长度,完美地规避了时间戳带来的潜在问题。
// 表示缓存30秒后失效,30秒内再次访问该资源,均使用本地的缓存,不再向服务器发起请求。
Cache-Control: max-age=30
Cache-Control 可以在请求头或者响应头中设置,并且可以组合使用多种指令,以下为响应指令:
-
public:响应可被任何中间节点(客户端和代理服务器)缓存。
-
private:只有客户端可以缓存,Cache-Control的默认取值。
-
max-age=< seconds >:表示缓存内容将在xxx秒后失效
-
s-maxage=< seconds >:同max-age作用一样,只在代理服务器中生效(比如CDN缓存),s-maxage优先级高于max-age,只对 public 缓存有效。设置了 s-maxage,没设置 public,代理服务器也可以缓存这个资源。
-
no-cache:可以缓存,但每次都应该去服务器验证缓存是否可用,进入协商缓存阶段。不使用 Cache-Control 的缓存控制方式做前置验证,而是使用 Etag 或者Last-Modified字段来控制缓存。相当于max-age:0, must-revalidate
-
no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存。
-
must-revalidate:可缓存但必须再向源服务器进确认
-
proxy-revalidate:要求中间缓存服务器对缓存的响应有效性再进行确认
例:
// 缓存的有效时间为100秒,之后访问需要向源服务器发送请求验证,此缓存可被代理服务器和客户端缓存。
Cache-Control: max-age=100, must-revalidate, public
协商缓存
当 Cache-Control 和 Expires 过期或者它的属性设置为no-cache时(即不走强缓存),那么浏览器第二次请求时就会与服务器进行协商,与服务器端对比判断资源是否进行了修改更新。
-
如果服务器端的资源没有修改(Not Modified),那么就会返回304状态码,告诉浏览器可以使用缓存中的数据。
-
如果数据有更新就会返回200状态码,服务器就会返回更新后的资源并且将缓存信息一起返回。
-
跟协商缓存相关的header头属性有(ETag/If-Not-Match 、Last-Modified/If-Modified-Since)请求头和响应头需要成对出现
Last-Modified 和 If-Modified-Since
浏览器在第一次访问资源,或缓存过期后访问,服务器返回资源的同时,在response header中添加 Last-Modified的header,值是这个资源在服务器上的最后修改时间,浏览器接收后缓存文件和header信息。
last-modified: Thu, 05 Jan 2017 07:20:31 GMT
随后我们每次请求时,会带上一个叫 If-Modified-Since 的时间戳字段,它的值正是上一次 response 返回给它的 last-modified 值:
If-Modified-Since: Thu, 05 Jan 2017 07:20:31 GMT
服务器接收到这个时间戳后,会比对该时间戳和资源在服务器上的最后修改时间:
-
如果没有变化,返回304和空的响应体,直接从缓存读取;
-
如果If-Modified-Since的时间小于服务器中这个资源的最后修改时间,说明文件有更新,于是返回新的资源文件和200
在如下场景, Last-Modified 还存在一些弊端:
-
我们编辑了文件,但文件的内容没有改变。服务端不知道我们是否真的改变了文件,仍然使用最后编辑时间来判断,那么我们再次请求该文件时,就会当做新资源重新请求一遍。不要请求的时候请求了一遍,这就不是我们想要的结果。
-
因为 Last-Modified 只能以秒计时,如果在不可感知的时间内修改完成文件,那么服务端会认为资源还是没变,不会发起请求获取正确的资源。该请求的时候不请求,这也不是我们希望的。
既然根据文件修改时间来决定是否缓存尚有不足,能否可以直接根据文件内容是否修改来决定缓存策略?所以在 HTTP / 1.1 出现了 ETag 和 If-None-Match
ETag 和 If-None-Match
Etag 是由服务器为每个资源生成的唯一的标识字符串,这个标识字符串是基于文件内容编码的,只要文件内容不同,它们对应的 Etag 就是不同的,反之亦然。因此 Etag 能够精准地感知文件的变化。
Etag 和 Last-Modified 类似,当首次请求时,我们会在响应头里获取到一个最初的标识符字符串:
ETag: W/"2261a-usg2EDVnIz4aDOjoAtc5Zd06TiI"
那么下一次请求时,请求头里就会带上一个值相同的、名为 if-None-Match 的字符串供服务端比对了:
If-None-Match: W/"2261a-usg2EDVnIz4aDOjoAtc5Zd06TiI"
Etag 与 Last-Modified 比较:
-
Etag 在感知文件变化上比 Last-Modified 更加准确
-
Etag 的生成过程需要服务器额外付出开销,会影响服务端的性能
-
当 Etag 和 Last-Modified 同时存在时,以 Etag 为准。
启发式缓存
如果Expires,Cache-Control: max-age,或 Cache-Control:s-maxage都没有在响应头中出现,并且设置了Last-Modified时,那么浏览器默认会采用一个启发式的算法,即启发式缓存:根据响应头中2个时间字段 Date 和 Last-Modified 之间的时间差值,取其值的10%作为缓存时间周期。这是浏览器默认的缓存方式
用户行为对缓存的影响
强制刷新,window下是Ctrl+F5,mac下就是command+shift+R
缓存的最佳实践
学习了那么多知识点,接下来我们来说下碰到实际的应用场景,该如何决策。
先看一下整体的流程图:
强缓存下,如果在xxx秒内,服务器上面的资源更新了,客户端在没有强制刷新的情况下,看到的内容还是旧的,如果这时前后端同步发布了新版本,后端接口更新了,有缓存的用户还在用旧的接口,这就会出问题。
如果我们每次都走协商缓存,每次都去请求判断是否是最新的资源,这样不就可以解决。可是这就违背了我们使用缓存的意图,同时也会增加服务器的负担。
所以我们希望我们用缓存就是尽可能多的命中强缓存,同时在有新版本更新的时候,让旧缓存失效!
频繁变动的资源,使用协商缓存
Cache-Control: no-cache
HTML 文件:
html页面缓存的设置主要是在< head >标签中嵌入< meta >标签,这种方式只对页面有效,对页面上的资源无效。
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="Cache-Control" content="max-age=7200">
不常变化的资源,静态资源,使用强缓存并配合文件名添加hash
Cache-Control: max-age=31536000
CSS,JS,图片:
给它们的 Cache-Control 配置一个很大的 max-age=31536000 (一年)
给文件名加上hash值:
webpack给我们提供了三种哈希值计算方式:
-
hash:跟整个项目的构建相关,构建生成的文件hash值都是一样的,只要项目里有文件更改,整个项目构建的hash值都会更改。(不用这个)
-
chunkhash:根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的hash值。
-
contenthash:由文件内容产生的hash值,内容不同产生的contenthash值也不一样。
其他一些关于缓存的知识点
缓存服务器版本
当Expires和Cache-Control:max-age=xxx同时存在的时候取决于缓存服务器应用的HTTP版本。应用HTTP/1.1版本的服务器会优先处理max-age,忽略Expires,而应用HTTP/1.0版本的缓存服务器则会优先处理Expires而忽略max-age。
Pragma
-
Pragma 是HTTP/1.0标准中定义的一个header属性,通常设置为Pragma:no-cache,请求中包含Pragma的效果跟在头信息中定义Cache-Control: no-cache相同。
-
通常定义Pragma以向后兼容基于HTTP/1.0的客户端。
-
在Chrome和Firefox中Pragma的优先级高于Cache-Control和Expires
-
浏览器勾选上disable cache时, 请求资源时,request headers自动带上了pragma字段
Vary
Vary 是一个HTTP响应头部信息,它用来告诉服务器要用哪些头部信息来返回资源。
- 如果你提供给移动端的内容是不同的,怎么让缓存服务器区分移动端和PC端呢?可以设置User-Agent字段来区分不同的客户端
Vary: User-Agent
- 源服务器启用了gzip压缩,但用户使用了比较旧的浏览器,不支持压缩,缓存服务器如何返回?
Vary: Accept-Encoding
age
response headers里面的 age 表示命中代理服务器的缓存. 它指的是代理服务器对于请求资源的已缓存时间, 单位为秒。如果文件被修改或替换,Age会重新由0开始累计。age值为0表示代理服务器刚刚刷新了一次缓存。
date
指的是响应生成的时间。
如果按F5频繁刷新发现响应里的Date没有改变,就说明命中了缓存服务器的缓存。
兼容性
- 在Firefox浏览器下,使用Cache-Control: no-cache 是不生效的,其识别的是no-store。这样能达到其他浏览器使用Cache-Control: no-cache的效果。所以为了兼容Firefox浏览器,经常会写成Cache-Control: no-cache,no-store。
后端设置缓存
强缓存:
res.setHeader('Cache-Control', 'public, max-age=xxx');
协商缓存:
res.setHeader('Cache-Control', 'public, max-age=0');
res.setHeader('Last-Modified', xxx);
res.setHeader('ETag', xxx);
参考
清理下缓存就好了
页面性能优化:前端缓存最佳实战
深入理解浏览器的缓存机制
一文读懂http缓存
HTTP缓存机制
HTTP 缓存
上一篇: springboot之跨域
下一篇: url模块