前言
缓存 , 一个用来加速数据访问的机制。通过把数据存储到一个 “离使用者更近的地方” ,实现更快地重复获取相同数据,从而加快页面的加载,带给用户更流畅的体验。
今天,我要给大家介绍缓存在前端领域的使用,讲讲浏览器自身的缓存,浏览器与服务器之间的沟通缓存,再讲讲有关代理服务器的缓存知识。
什么是浏览器缓存?
在浏览器中,当我们点击访问一个网站时,网站会加载对应的html、css、js、图片等文件。浏览器会将那些不怎么变化的文件资源保存到本地缓存中。这样当我们刷新或是再次访问该网页时,就可以直接从浏览器缓存获取资源,减少了向服务器发起请求的数量,增加了网页的开启速度,也减少了服务器的压力。
如上图,Network 面板中的 Size 栏有两种标识,标明了哪些资源是来自 memory cache(内存缓存)或 disk cache(硬件缓存)。我们可以看到Size栏为 memory cache 的文件的加载速度非常快,都是0ms, disk cache 的文件加载速度基本上都在5~25ms左右,加载速度也远快于其它从服务器获取的资源。
memory cache 和 disk cache 是两种缓存方式。memory cache是内存缓存,简单来说,就是非常快。只是缓存的时间很短,缓存空间小。
disk cache 是硬件缓存,它没有内存缓存那么快,但是它胜在缓存时间更长,缓存量更大。
浏览器缓存流程
常用缓存策略
前端的基本缓存策略分为两种:强缓存和协商缓存。它们两者的本质区别就在于对缓存做决策的“人”不一样。强缓存由客户端决定,协商缓存由服务器决定。
强缓存
简单来说,强缓存就是在客户端给缓存记个时,时间没到,请求就走缓存,时间到了,就重新从服务器获取数据。
强缓存总共有两种方案:Exprise和Cache-Control。
Exprise
Exprise是http1.0里控制缓存的的字段。exprise的值是就是缓存的过期时间。
比如说: Exprise:Fri,21 Nov 2025 08:00:00 GMT
它有一个非常大的缺点,就是它的时间戳是本地的时间戳。也就是说,假如我们把本地的时间改到Fri,21 Nov 2025 08:00:00 GMT,上面的这个缓存的过期了。这也会造成很大的隐患。
所以,Exprise已经被弃用了……
既然Exprise这么废,当然就会有另一种强缓存的方式来替代咯。那就是……Cache-Control
Cache-Control
Cache-Control这个字段在http1.1中被添加。Cache-Control完美解决了exprise本地时间和服务器时间不同步的问题。也是当下强缓存最普遍的方法。
Cache-Control的用法很简单,在资源的响应头上写需要缓存多久就好了。
暂时无法在飞书文档外展示此内容
如上图,在Cache-Control后写 max-age=20就表示缓存时间为20秒。往后的20秒内如果资源再次被请求,则会直接从缓存中获取。
Cache-Control中因为max-age后面的值是一个滑动时间,从服务器第一次返回该资源时开始倒计时。所以也就不需要比对客户端和服务端的时间,解决了Exprise所存在的巨大漏洞。
Cache-Control中有八个属性:max-age,s-maxage,no-cache,no-store,private,public,
must-revalidate,proxy-revalidate。
max-age: 决定客户端资源被缓存多久。
s-maxage: 决定代理服务器缓存的时长。
no-cache: 表示是强制进行协商缓存。
no-store: 是表示禁止任何缓存策略。
public: 表示资源即可以被浏览器缓存也可以被代理服务器缓存。
private: 表示资源只能被浏览器缓存。
must-revalidate: 强制到服务端验证。
proxy-revalidate: 要求代理缓存服务验证有效性。
no-cache和no-store
no-cache ,它并不像字面意思一样禁止缓存,实际上,no-cache的意思是跳过强缓存,强制进行协商缓存。如果某一资源的Cache-Control中设置了no-cache,那么该资源会直接跳过强缓存的校验,直接去服务器进行协商缓存。而no-store就是禁止所有的缓存策略了。
注意:no-cache和no-store是两个互斥属性,不能同时出现在Cache-Control中。
public和private
一般情况下,客户端可以直接与服务器进行交互。但是某些情况下,中间会出现代理服务器。
而public和private就是决定资源是否可以在代理服务器上缓存。其中, public表示资源在客户端和代理服务器都可以被缓存;private表示资源只能在客户端被缓存,无法在代理服务器缓存。
注意:如果两个属性都没有声明,默认为 private 属性。同样,public和private也是一组互斥属性。
max-age和s-maxage
max-age表示缓存在客户端储存的最大时间。s-maxage表示缓存在代理服务器缓存的时长。当然,因为是在代理服务器上的操作,s-maxage和public属性必须同时出现。
注意:max-age和s-maxage并不互斥,可以同时出现。
must-revalidate:
与 no-cache 类似,强制到服务端验证。它是表示在一个缓存过期之后,不能直接使用这个过期的缓存,必须校验之后才能使用。因为 HTTP 规范是允许客户端在某些特殊情况下直接使用过期缓存的,所以适用于一些特殊场景,例如发送了校验请求,但发送失败了之类。
proxy-revalidate:
要求代理缓存服务验证有效性。代理服务器在使用缓存之前也必须与服务器重新验证其有效性,确保不会提供陈旧的数据。
那么如何使用这些属性呢?用逗号分割就好了。就像这样:
暂时无法在飞书文档外展示此内容
注意,当Cache-Control和Exprise同时存在时,会优先使用Cache-Control属性。
以上就是强缓存的内容了
协商缓存
相比于强缓存来说,协商缓存对缓存的处理方式会更加智能。协商缓存就是浏览器会带着特定的标识向浏览器发起请求。浏览器根据标识来决定缓存是否更新。协商缓存分两种:Last-Modified 和 ETag。
协商缓存的两种状态
1)资源未变化,返回304
2)资源已更新,返回200
当然,使用哪一种缓存策略来优化性能还是要根据实际的项目需求而定。
不过,毕竟强缓存和协商缓存是在浏览器缓存中的操作,它们具体的实现方式可能会因浏览器和服务器的不同而有所差异。在某些情况下,即使使用ETag值也可能会出现缓存没有及时更新的问题。所以接下来,我们离开浏览器本身,使用代理服务器对浏览器进行缓存优化。
Last-Modified
Last-Modified记录了文件在后端最后被修改的时间,通过对比资源的修改时间,确认是否需要更新缓存。
验证流程:
1)第一次访问页面或是资源更新时,会在响应头返回 Last-Modified 字段
2)客户端在发起请求时,会在请求头上加上If-Modified-Since字段,字段内容是上次请求返回的Last-Modified值。
3)服务器会对比Last-Modified和If-Modified-Since字段。若服务器上的时间大于 Last-Modified 的值,则重新返回资源,返回200,表示资源已更新;反之则返回304,代表资源未更新,可继续使用缓存。
到这里要提一嘴,强缓存和通过 Last-Modified 的协商缓存,本质上大差不差,都是用时间戳来标记判断缓存资源。所以它们会存在漏洞,而漏洞就是来自于这种基于时间的比较方式。
不论是强缓存还是 Last-Modified ,时间设置的单位都是秒(s),所以当服务器的数据以小于秒的频率更新数据时,就会造成更新滞后的问题。在某些场景下,先前的缓存策略就不适用了。
那么,有什么方案可以让我们避开时间漏洞,拿到准确的内容呢?
那就要提到我们的ETag了。
Etag
其实ETag和上面的策略的用法差不多,只是Etag的比较标识,从 时间戳 变成了 文件指纹
“文件指纹”:根据文件内容计算得出的唯一哈希值。这个哈希值会随着文件内容的改变而改变。
然后我们来看看ETag的验证流程:
1)第一次请求某资源时,服务器会读取文件并计算出文件指纹,文件指纹会在响应头的 ETag 字段返回。
2)客户端在发起请求时,会在请求头上加上If-None-Match字段,字段内容是上次请求返回的 ETag 值。
3)服务器拿到请求头后,会拿到 If-None-Match 字段,并再次读取目标资源并生成 ETag 指纹。将两个指纹对比,指纹相同,标识文件未被修改,返回304状态码,使用缓存。若指纹不匹配,说明文件已修改,则会把新的文件返回,状态码为200,并把新的文件指纹存到响应头的 ETag 字段里。
那么,ETag是如何生成的呢?
ETag的生成方式通常有这几种:
1)基于内容的哈希值生成:通过hash算法(MD5、SHA-256等)根据文件的内容给对应的文件生成对应的hash值,这个值就是ETag的标识。
2)ContentLength+Last-Modified:内容长度+时间戳的形式作为ETag值(nginx默认生成方式)
3)基于版本号生成:使用资源对应的版本号作为ETag值(如果有的话)
ETag 也分强验证与弱验证。强验证就是将哈希值的计算深入到每个字节,生成非常精确的指纹。这也需要更大的计算量。而弱验证,就是提取文件的部分属性生成哈希值,它的速度会比强验证快,所需的计算量也会小,但就没有那么准确了。
ETag的缺点
当然,ETag 也有缺点。文件指纹的生成本身就需要造成更多的计算开销。如果文件尺寸大,数量多,并且请求频繁,如果使用强验证生成ETag,那么 ETag 的计算量就会很大,影响服务器的性能。而很多服务器(比如nginx)的默认ETag生成方式是上面提到的第二种:ContentLength+Last-Modified。(当然可以自己定义ETag生成方式)这种方式会更轻便,但同样精确度会下降,这就有可能造成实际资源不会更新的问题。
那怎么解决呢。
使用nginx
下面我们来介绍一下nginx
nginx是一种反向代理服务器。通过 nginx ,我们可以在代理服务器端优化缓存
方法一:在这里,我们可以对部分.html文件设置 不缓存。(只有当html中的资源路径发生变化时,才会向服务器重新获取资源。一般情况下,直接调用本地缓存)通过这种方案,可以确保用户每次得到的都是最新的资源。若资源为更新,也能使用缓存的数据,快速响应
如何设置?就是在nginx中添加对html文件的禁用缓存限制。
location ~ ^/static/(admin1|admin2|admin3)/ {
if ($request_filename ~* .*.(?:htm|html)$) {
add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
}
}
方法二:负载均衡
因为ETag的生成本身就是一个需要消耗服务器算力的问题。若文件数量过多,体积过大,就会需要大量的开销。或者比如说:我们现在有一个网站部署到一个tomcat服务器上,让这个服务器监听80端口,一开始访问量不大,没有问题。但当用户多起来,对服务器的请求数量上来了,这样一个tomcat就处理不过来了。那我们如果要再开一个其他的端口,但这样也不太行。
这个时候,nginx就派上大用场了。
我们可以使用nginx的反向代理来实现上面的这个负载均衡问题。我们只需要代理两个tomcat进程,配置负载均衡策略,然后将入口交给nginx来管理,那么用户在访问同一个地址的时候就可以将请求分发到不同的tomcat上以实现负载均衡。
简单的配置如下:
http {
upstream server1{
server 127.0.0.1:8080 weight=3;
server 127.0.0.1:8081 weight=2;
}
server {
listen 80;
location / {
proxy_pass http://server1;
}
}
}
其中的 weight 表示权重,可以控制流向端口的占比。
nginx的另一个核心功能就是 限流 ,可以限制正常访问频率、限制突发访问频率、限制并发访问频率。
缓存的其它使用场景
除了这些缓存策略之外,我们还可以使用Service Worker、配置CDN域名缓存、redis缓存等等。
总结
缓存对于前端开发者来说是一个不可或缺的知识,它在性能优化上可以做出许多贡献。虽然可能未来实际工作中缓存优化的任务是交给后端来做的,但是前端总是要和后端沟通的,成为一名懂后端的前端无疑对未来的工作会有很大帮助。或者功利地讲,对面试也可能会起到奇效。