面试常客:HTTP 缓存

简介: 速度、速度,还是速度,一个网站要想体验好,就必须在第一时间以最快的速度显示出来。mysql查询慢,就加一层 redis 做缓存,网站资源加载慢,怎么做,使用 HTTP缓存

速度、速度,还是速度,一个网站要想体验好,就必须在第一时间以最快的速度显示出来。mysql查询慢,就加一层 redis 做缓存,网站资源加载慢,怎么做,使用 HTTP缓存

HTTP缓存自 HTTP/1.0 就开始有,为的是减少服务器压力,加快网页响应速度


缓存操作的目标



HTTP 缓存只能存储 GET 请求的响应,而对其他类型的请求无能为力


缓存发展史



HTTP/1.0 提出缓存概念,即强缓存 Expires 和协商缓存 Last-Modified。后 HTTP/1.1 又有了更好的方案,即强缓存 Cache-Control 和协商缓存 ETag


为什么 Expires 和 Last-Modified 不适用呢?


Expires 即过期时间,但问题是这个时间点是服务器的时间,如果客户端的时间和服务器时间有差,就不准确。所以用 Cache-Control 来代替,它表示过期时长,这就没歧义了

Last-Modified 即最后修改时间,而它能感知的单位时间是秒,也就是说如果在1秒内改变多次,内容文件虽然改变了,但展示还是之前的,存在不准确的场景,所以就有了 ETag,通过内容给资源打标识来判断资源是否变化


以下表格利于对比理解


版本 强缓存 协商缓存
HTTP/1.0 Expires Last-Modified
HTTP/1.1 Cache-Control ETag


两大缓存类型对比



前文已介绍不同版本下的缓存类型。当时提了有一句强缓存和协商缓存,但没具体介绍。现在来讲讲这两种缓存类型


强缓存


Cache-Control


  • HTTP/1.1


  • 通过过期时长控制缓存,对应的字段有很多,例如 max-age


  • 例如 Cache-Control: max-age=3600,表示缓存时间为3600秒,过期失效


  • 缓存请求指令:


Cache-Control: max-age=<seconds>
Cache-Control: max-stale[=<seconds>]
Cache-Control: min-fresh=<seconds>
Cache-control: no-cache
Cache-control: no-store
Cache-control: no-transform
Cache-control: only-if-cached


  • 缓存响应指令:


Cache-control: must-revalidate
Cache-control: no-cache
Cache-control: no-store
Cache-control: no-transform
Cache-control: public
Cache-control: private
Cache-control: proxy-revalidate
Cache-Control: max-age=<seconds>
Cache-control: s-maxage=<seconds>


  • 其中关键点:


  • 一般用于缓存静态资源
  • public:响应可以被中间代理、CDN 等缓存
  • private:专用于个人的缓存,中间代理、CDN等能换缓存此响应
  • max-age:单位是秒
  • 不使用缓存(包括协商缓存)
  • 跳过当前的强缓存,发送 HTTP 请求(如有协商缓存标识即直接进入协商缓存阶段
  • no-cache 的含义和 max-age=0 一样 ,即跳过强缓存,强制刷新
  • Cache-control: no-cache
  • Cache-control: no-store
  • Cache-Control: public, max-age=31536000


更多指令参考指令大全[1]


Expires


  • HTTP/1.0


  • 语法:


Expires: <http-date>


  • 即过期时间,存在于服务器返回的响应头里


  • Expires: Mon, 11 Apr 2022 06:57:18 GMT


  • 表示资源在2022年4月11号6点57分过期,过期了就会往服务端发请求


  • 如果在Cache-Control响应头设置了 "max-age" 或者 "s-max-age" 指令,那么 Expires 头会被忽略


  • 缺点:服务器时间与浏览器时间可能不一致


更多指令参考指令大全[2]


Cache-Control VS Expires


  • Cache-Control 较之 Expires 更为精准


  • 同时存在时,Cache-Control 优先级大于 Expires


  • Expires 是 HTTP/1.0 提出,其浏览器兼容性更好,Cache-Control 是 HTTP/1.1 提出,可同时存在,当有不支持 Cache-Control 的浏览器时会以 Expires 为准


协商缓存


协商缓存需要配合强缓存使用,使用协商缓存的前提是设置强缓存设置 Cache-Control: no-cache或者 pragma: no-cache或者 max-age=0  告诉浏览器不走强缓存


pragma 是 HTTP/1.0 中禁止网页缓存的字段,其取值为 no-cache 和 Cache-Control 的 no-cache 效果一样


ETag/If-None-Match


  • HTTP/1.1


  • 即生成文件唯一标识来判断是否过期。只要内容改变,这个值就会变


  • If-None-Match 配合,ETag是请求服务器后返回给每个资源文件的唯一标识,客户端会将此标识存在客户端(即浏览器)中,下次请求时会在请求头的 If-Nono-Match 中将其值带上,服务器判断 If-None-Match 是否与自身服务器上的 ETag 一致,如果一致则返回 304,重定向跳转使用本地缓存;不一致,则返回200,将最新资源返回给客户端,并带上 ETag


  • 更多指令参考指令大全[3]


Last-Modified/If-Modified-Since


  • HTTP/1.0


  • 最后修改时间,即通过最后修改时间来判断是否过期。在浏览器第一次给服务器发送请求后,服务器会在响应头上加上这个字段


  • If-Modified-Since 配合,客户端访问服务器资源时,服务器端会将 Last-Modified 放入响应头中,即这个资源在服务器上的最后修改时间,客户端缓存这个值,等下次请求这个资源时,浏览器会检测到请求头中的 Last-Modified,于是乎添加 If-Modified-Since,如果 If-Modified-Since 的值与服务器中这个资源的最后修改时间一致,则返回 304,重定向跳转使用本地缓存;不一致,则返回200,将最新资源返回给客户端,并带上 Last-Modified


  • 缺点:


  • 文件虽然被修改,但最后的内容没有变化,这样文件修改时间还是会更新


  • 有些文件修改频率在秒以内,这样以秒粒度来记录就不适用了


  • 有些服务器无法精准获取文件的最后修改时间


  • 更多指令参考指令大全[4]


ETag VS Last-Modified


  • 精确度


  • ETag > Last-Modified。ETag 是通过内容给资源打标识来判断资源是否变化,而 Last-Modified不一样,在某些场景下准确度会失效。例如编辑文件,但是文件内容未变,缓存会失效;或者在1秒内改变多次,Last-Modified能感知的单位时间是秒


  • 性能


  • Last-Modified > ETag。Last-Modified 仅仅记录一个时间点,而 ETag需要根据文件的具体内容生成哈希值


  • 如果两个都支持的话,服务器会优先选择ETag


协商缓存的条件请求


前文说到协商缓存是在请求头添加 If-None-MatchIf-Modified-Since,这些请求头是什么,添加有什么用?


强缓存是通过具体时间到期或过期时长来控制缓存,这就有个问题了,如果其中的一些文件修改了,因为强缓存,浏览器展示的还是原来的数据,所以对那种常变化的数据不能使用强缓存做缓存策略,于是乎,就有了协商缓存,通过文件变化告诉浏览器缓存失效,使用前需去服务器验证是否是最新版?


这样,浏览器就要连续发送两个请求来验证:


  1. 先是 HEAD 请求,获取资源的修改时间、hash值等元信息,然后与缓存数据比较,如果没有改动就使用缓存


  1. 否则就再发一个 GET 请求,获取最新的版本


但这样的两个请求的网络成本太高,所以 HTTP 协议就定义了一系列 If 开头的条件请求字段,专门用来检查验证资源是否过期,把两个请求合并在一个请求中做。而且验证的责任也交给服务器


  • If-Modified-Since:和 Last-modified 比较,是否已经修改了


  • If-None-Match:和 ETag 比较,唯一标识是否一致


  • If-Unmodified-Since:和 Last-modified 对比,是否修改


  • If-Match:和 ETag 比较是否匹配


  • If-Range


其中,最常见的当属是 If-Modified-Since 和 If-None-Match。它们分别对应Last-Modified 和 ETag。需要第一次的响应报文预先提供 Last-Modified 和 ETag,然后第二次请求时就可以带上缓存里的原址,验证资源是否是最新的


如果资源没有变,服务器就回应一个 304 Not Modified ,表示缓存依然有效,浏览器就可以更新一个有效期,然后使用缓存了


image.png

缓存流程


什么时候用强缓存,什么时候用协商缓存?


首先强缓存的权重大于协商缓存,当强缓存存在时,协商缓存只能看着;其次 HTTP/1.1 中的缓存标识符大于 HTTP/1;所以当 Cache-Control 存在时,看它的,如果它不存在,则看 Expires,如果将强缓存设置为 Cache-Control:no-cacheCache-Control:max-age=0pragma: no-cache,即告诉浏览器不走强缓存,则进入协商缓存。


判断上次响应中是否有ETag,如果有,则发起请求,请求头中带有条件请求 If-None-Match,如果没有,则再判断上次响应中是否有Last-Modified,如果有,则发起请求头中带If-Modified-Since 的条件请求,如果没有,则说明没有协商缓存,发起 HTTP 请求即可。无论是带If-None-Match的请求还是 If-Modified-Since 的请求,都会返回状态(由服务器端判读资源是否变化),如果是304,说明缓存资源未变,使用本地缓存;如果是200,则说明资源改变,发起 HTTP 请求,并记住响应头中的 ETag/Last-Modified


大致流程图如下所示:


image.png

缓存判断流程图


那么哪些资源要采用强缓存,哪些资源采用协商缓存呢?


像静态资源这类我们长期不会去变动的资源应该用强缓存,不难理解;而像我们常修改的文件应该采用协商缓存,如果资源没变,那么当用户第二次进去还是用该资源,如果资源修改,用户进入发起 HTTP 请求获取最新资源


我们在访问网站时,如果留心都能在 F12 中观察到一二。如图所示,我的五年前端三年面试[5]放在 github 服务器上,F12进入 Network中,能看到返回头中的信息。

Cache-Control、Expires、ETag、Last-Modified都存在


image.png

五年前端三年面试


缓存位置



上文中常提到无论使用强缓存还是协商缓存,都会从浏览器本地中获取,那么浏览器的本地存储是存在哪里,他们又有什么分类呢?


按照缓存位置分类,分为四处,Memory Cache(内存缓存)、Disk Cache(硬盘缓存)、Service Worker、Push Cache


Memory Cache


因为内存有限,并不是所有的资源文件都会放在内存里缓存,它主要用来缓存有 preloader 相关指令的资源,比如<link rel="prefetch">。preloader 可以一边解析 js/css 文件,一边网络请求下一个资源


Disk Cache


磁盘上的缓存。在所有浏览器缓存中,disk cache 覆盖面最大,它会根据 HTTP Header 中的字段判断哪些资源需要缓存,哪些资源已经过期需要重新从服务器端请求


Service Worker


独立线程,借鉴了 Web Worker 的思路。即让 JS 运行在主线程之外,由于它脱离浏览器窗口,因为无法直接访问DOM,但是它还是能做很多事情,如


  • 离线缓存,Service Worker Cache


  • 消息推送


  • 网络代理


  • 它是PWA的重要实现机制


Push Cache


即推送缓存,浏览器中的最后一道防线,HTTP2中的内容


优先级:Service Worker-->Memory Cache-->Disk Cache-->Push Cache。


实践



说了这么多理论知识,等实战的时候却一头雾水,怎么破?


以上皆为口舌之辩,唯有实践出真章(以上皆为面试之辩,唯有实践出本事)


目前前端项目都是以 webpack 或类 webpack 工具库打包,在 webpack 中配置哈希,前端方面的缓存工作就完成了


我们要实现的效果是:


  • HTML:协商缓存


  • CSS、JS、图片等资源:强缓存,文件名带上hash


webpack 中的哈希有三种:hash、chunkHash、contentHash


  • Hash:和整个项目的构建相关,只要项目文件有改变,整个项目构建的 hash 值就会改变


  • chunkHash:和 webpack 打包的 chunk 有关,不同的入口会生成不同的 chunkHash值


  • contentHash:根据文件内容来定义hash,文件内容不变,则 contentHash 不变

这边需要把 CSS 用 contentHash 处理,其他资源用 chunkHash 做处理


非前端工程化项目


即传统的前端页面,一般放在静态服务器中,那么就要对修改的文件做版本控制,例如在入口文件 index.js 上加版本号(index-v2.min.js)或者加时间戳(time=1626226),以此做缓存策略


后端缓存实践


真正起到缓存作用的是在后端,后端来设置缓存策略,告诉浏览器能否做缓存。这里我们对强缓存和协商缓存做个demo来实验下,


强缓存方案


代码如下:


const express = require('express');
const app = express();
var options = { 
  etag: false, // 禁用协商缓存
  lastModified: false, // 禁用协商缓存
  setHeaders: (res, path, stat) => {
    res.set('Cache-Control', 'max-age=10'); // 强缓存超时时间为10秒
  },
};
app.use(express.static((__dirname + '/public'), options));
app.listen(3008);


PS:代码来源自:图解 HTTP 缓存[6],在做测试时,需要注意,强缓存下,刷新页面是测不出来,点击后返回方能有效


image.png

强缓存效果


协商缓存方案


代码如下:


const express = require('express');
const app = express();
var options = {
    etag: true, // 开启协商缓存
    lastModified: true, // 开启协商缓存
    setHeaders: (res, path, stat) => {
        res.set({
            'Cache-Control': 'max-age=00', // 浏览器不走强缓存
            'Pragma': 'no-cache', // 浏览器不走强缓存
        });
    },
};
app.use(express.static((__dirname + '/public'), options));
app.listen(3001);


效果如下:


image.png

协商缓存效果


总结



HTTP 为什么要缓存,为了分担服务器压力,也为了让页面加载更快


有什么手段?HTTP 的强缓存和协商缓存,强缓存作用于那些不怎么变化的资源(如引入的库,js,css等),协商缓存适用常更新的文件(例如 html)


强缓存是什么?在 HTTP/1.0 中以 Expires 为依据,但它不准确,HTTP 协议升级成1.1后,用新标识符 Cache-Control 来代替,但两者可以同时存在,Cache-Control 的权重更大一些


协商缓存是什么?在 HTTP/1.0 中以 Last-Modified 为依据,即最后过期修改时间,它也不准确,HTTP升级成1.1后,用新标识符 ETag 来代替,两者可同时存在,后者的权重更大


无论是 Expires ,还是 Last-Modified,都是以时间点来依据,理论上是不出问题,但却出问题了,所以就有了新的方案


其中强缓存存在时,浏览器会采用强缓存标识符来缓存,当将强缓存设置为失效时,浏览器则会采用协商缓存来做缓存策略


以上,即使笔者所理解的 HTTP 缓存


附上demo地址[7]


参考资料



  • 深入理解浏览器的缓存机制[8]


  • 彻底理解浏览器的缓存机制[9]




  • node 实践彻底搞懂强缓存和协商缓存[10]


  • 浅析 HTTP 缓存[11]


  • MDN web docs[12]


  • 图解 HTTP 缓存[13]


[1] 指令大全: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Cache-Control


[2] 指令大全: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Expires


[3] 指令大全: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/ETag


[4] 指令大全: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Last-Modified


[5] 五年前端三年面试: https://fe.azhubaby.com/


[6] 图解 HTTP 缓存: https://juejin.cn/post/6844904153043435533


[7] demo地址: https://github.com/johanazhu/demo/tree/master/%E7%BD%91%E7%BB%9C%E7%BC%93%E5%AD%98%E7%AD%96%E7%95%A5


[8] 深入理解浏览器的缓存机制: https://www.jianshu.com/p/54cc04190252


[9] 彻底理解浏览器的缓存机制: https://www.cnblogs.com/duiniweixiao/p/8884274.html


[10] node 实践彻底搞懂强缓存和协商缓存: https://juejin.cn/post/6942264171870289956


[11] 浅析 HTTP 缓存: https://juejin.cn/post/6944891188826603528


[12] MDN web docs: https://developer.mozilla.org/zh-CN/


[13] 图解 HTTP 缓存: https://juejin.cn/post/6844904153043435533

相关文章
|
19天前
|
缓存 NoSQL 关系型数据库
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
本文详解缓存雪崩、缓存穿透、缓存并发及缓存预热等问题,提供高可用解决方案,帮助你在大厂面试和实际工作中应对这些常见并发场景。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
|
1月前
|
SQL 缓存 关系型数据库
美团面试:Mysql 有几级缓存? 每一级缓存,具体是什么?
在40岁老架构师尼恩的读者交流群中,近期有小伙伴因未能系统梳理MySQL缓存机制而在美团面试中失利。为此,尼恩对MySQL的缓存机制进行了系统化梳理,包括一级缓存(InnoDB缓存)和二级缓存(查询缓存)。同时,他还将这些知识点整理进《尼恩Java面试宝典PDF》V175版本,帮助大家提升技术水平,顺利通过面试。更多技术资料请关注公号【技术自由圈】。
美团面试:Mysql 有几级缓存? 每一级缓存,具体是什么?
|
4月前
|
缓存 JSON 前端开发
超详细讲解:http强缓存和协商缓存
超详细讲解:http强缓存和协商缓存
|
27天前
|
存储 缓存 网络协议
计算机网络常见面试题(二):浏览器中输入URL返回页面过程、HTTP协议特点,GET、POST的区别,Cookie与Session
计算机网络常见面试题(二):浏览器中输入URL返回页面过程、HTTP协议特点、状态码、报文格式,GET、POST的区别,DNS的解析过程、数字证书、Cookie与Session,对称加密和非对称加密
|
2月前
|
存储 缓存 NoSQL
保持HTTP会话状态:缓存策略与实践
保持HTTP会话状态:缓存策略与实践
|
2月前
|
缓存 监控 算法
小米面试题:多级缓存一致性问题怎么解决
【10月更文挑战第23天】在现代分布式系统中,多级缓存架构因其能够显著提高系统性能和响应速度而被广泛应用。
46 3
|
2月前
|
存储 缓存 NoSQL
阿里面试题:缓存的一些常见的坑,你遇到过哪些,怎么解决的?
阿里面试题:缓存的一些常见的坑,你遇到过哪些,怎么解决的?
|
2月前
|
存储 缓存 监控
HTTP:强缓存优化实践
HTTP强缓存是提升网站性能的关键技术之一。通过精心设计缓存策略,不仅可以显著减少网络延迟,还能降低服务器负载,提升用户体验。实施上述最佳实践,结合持续的监控与调整,能够确保缓存机制高效且稳定地服务于网站性能优化目标。
49 3
|
3月前
|
存储 缓存 Android开发
Android RecyclerView 缓存机制深度解析与面试题
本文首发于公众号“AntDream”,详细解析了 `RecyclerView` 的缓存机制,包括多级缓存的原理与流程,并提供了常见面试题及答案。通过本文,你将深入了解 `RecyclerView` 的高性能秘诀,提升列表和网格的开发技能。
75 8