主题
HTTP缓存
缓存
缓存的原理: 是在首次请求之后保存一份请求资源的响应副本,当用户再次发起相同请求后,如果判断缓存命中则拦截请求,将之前存储的副本返回给用户,,从而避免重新向服务器发起网络资源请求
缓存好处: 减少网络带宽的浪费,加快资源响应速度,提高网站性能,从而影响用户使用体验 若用户使用的是按量计费的方式访问网络,可以避免多余的网络请求增加用户的网络流量资费。
缓存分类
1. 缓存技术种类
代理缓存 浏览器缓存 网关缓存 负载均衡器及内容分发网络
2. 缓存分类
共享缓存 指缓存内容可以被多个用户使用。比如:公司内部架设的web代理,代理缓存,网关缓存等 私有缓存 指只能单独被用户使用的缓存。比如:浏览器缓存
Http缓存
Http缓存是前端开发中最长接触的缓存机制之一,可以细分为强缓存
和协商缓存
。二者最大的区别在于判断缓存命中时,浏览器是否要向服务器端进行询问以协商缓存的相关信息,进而判断是否需要就响应内容进行重新请求。
Cache-Control【在HTTP/1.1版本中】
cache-control
在缓存中是非常重要的字段,用来指定Response-Request遵循的缓存机制。
1. cache-control
作为请求头时
属性名:cache-control
(1)Cache-Control: no-cache
使用no-cache
指令的目的是为了防止从缓存中返回过期的资源。 客户端发送的请求中如果包含 no-cache
指令,则表示 客户端将不会接收缓存的资源。每次请求都是从服务器获取资源,返回304。
(2)Cache-Control: no-store
使用no-store
指令表示请求的资源不会被缓存,下次任何其它请求获取该资源,还是会从服务器获取,返回 200,即资源本身。
2. cache-control
作为响应头时
属性名:cache-control
Cache-Control: public
当指定使用public
指令时,为共享缓存,既可以被浏览器缓存,也可以被代理服务器缓存。Cache-Control: private
若未显示指定public
与private
则默认为private
当指定private
指令后,响应只能被浏览器缓存,这与public
指令的行为相反。 若在缓存服务器上设置了private,缓存服务器会对该特定用户提供资源缓存的服务,对于其他用户发送 过来的请求,代理服务器则不会返回缓存。Cache-Control: no-cache
设置对比缓存(协商缓存) 如果服务器返回的响应中包含no-cache
指令,每次客户端请求,必需先向服务器确认其有效性,如果资源没有更改,则返回304,若资源更改了则返回新资源。Cache-Control: no-store
设置不缓存(也可以直接不设置缓存字段,默认就是无缓存) 不对响应的资源进行缓存,即用户下次请求还是返回 200,返回资源本身。Cache-Control: max-age=604800
(单位:秒) 设置强缓存 资源缓存在本地浏览器的相对时间,如果超过该时间,则重新向服务器获取。max-age
和s-maxage
max-age
属性值会比s-maxage
更常用max-age
表示服务器告知浏览器响应资源的过期时长。在一般项目的使用场景中基本够用。 对于大型架构的项目通常会涉及使用各种代理服务器的情况,这就需要考虑缓存在代理服务器上的有效性问题,这便是s-maxage
存在的意义s-maxage
表示缓存在代理服务器中的过期时长,且仅当设置了public
属性值时才有效
为了兼容性,在http1.0
中可以使用expires
属性实现,但是expires
依赖浏览器本地时间。
强缓存
原理:浏览器判断所请求的目标资源有效命中强缓存,则可以直接从强缓存中返回请求响应,无须与服务器进行任何通信。
在响应头中,与强缓存相关的字段是expires
和cache-control
1.强缓存- Expires
expires
是在HTTP 1.0
协议中申明的用来控制失效日期时间戳的字段,它由服务器端指定后通过响应头告诉浏览器,浏览器在接收到带有该字段的响应体后进行缓存
expires指定的时间是绝对时间
nodejs中用expires
设置强缓存示例
js
// Expires大小写都可以
res.writeHead(200,{
// expires: new Date('2025-12-20 12:12:12').toUTCString() // 强缓存到期时间 2025-12-20 12:12:12
Expires: new Date(new Date().getTime() + 50 * 1000).toUTCString() // 做强缓存50秒
})
- expires的缺点
expires
严重依赖本地时间,若客户端本地时间与服务器时间不同步,那么判断缓存过期时间就不能大道预期
2.强缓存- Cache-Control
cache-control
既可以做强缓存,也可以做协商缓存
由于Expires
判断存在局限性,从HTTP 1.1开始新增了cache-control
字段来对expires
的功能进行扩展和完善。
cache-control指定的时间是相对时间,单位是秒
nodejs中用cache-control
设置强缓存示例
js
// cache-control大小写都可以
res.writeHead(200,{
'cache-control': 'max-age=50' // 50秒后过期,相对时间
})
一旦做了强缓存50s,那么在这缓存时间的50s内,浏览器将不会再想服务端进行请求,而是直接从本地取数据渲染。(当然,你不能断网,如果断网了也想成功渲染的话就需要使用离线缓存方案了)
对于应用程序中不会改变的文件,即通常可以在发送响应头前添加积极缓存。这包括例如由应用程序提供的静态文件,例如图像,css文件和js文件,
js
'Cache-Control':'public, max-age=3153600'
3.强缓存方案
由上可知,强缓存可以设置expires
或cache-control:max-age=30
来实现,并且cache-control
能作为expires
的完全替代方案,并拥有其所不具备的一些缓存控制特性,在项目中使用cache-control
就够了,目前expires
还存在的唯一理由是考虑可用性方面的向下兼容。
协商缓存
原理:在使用本地缓存之前,需要向服务器端发起一次GET请求,与之协商当前浏览器保存的本地缓存是否已经过期。
作用:可以帮助我们解决强缓存下资源不更新的问题
注意:协商缓存必须设置cache-control:no-cache
才会生效
1.协商缓存-last-modified
和if-modified-since
原理:设置了协商缓存,首次请求资源时候,服务器会在响应头中设置一个last-modified
字段,字段的值是请求的资源的最后修改时间,当再次请求资源时候,浏览器需要向服务器发送一次GET请求确认资源是否有更新,这个请求的请求头中会包含If-Modified-Since
字段,该字段值正是资源的上次修改时间。服务器收到请求后,将资源的最后修改时间与请求头中的if-modified-since
属性值对比,一致就命中协商缓存,返回304状态码,告诉浏览器继续使用本地文件,不会返回资源,否则返回全新资源文件。
nodejs设置last-modified
协商缓存示例
js
{
/*.......*/
const { mtime } = fs.statSync("./2.jpg"); // 获取文件状态,mtime是文件最后修改时间
const ifModifiedSince = req.headers["if-modified-since"];
if (ifModifiedSince === mtime.toUTCString()) {
// 命中协商缓存
res.statusCode = 304;
res.end();
return;
}
// 没命中缓存
const data = require("./2.jpg");
res.writeHead(200, {
"cache-control": "no-cache", // 必须设置no-cache才是协商缓存
"last-modified": mtime.toUTCString(), // last-modified
});
res.end(data);
return;
/*.......*/
}
last-modified
的不足:
- 他只是根据资源的修改时间戳进行判断,若资源的文件名改变内容不变的情况,也会造成修改时间戳改变,会导致缓存失效。
last-modified
的时间是秒,若是资源修改的时间很短暂,假设在几百毫秒内完成,last-modified
是无法是识别的
++last-modified
可以满足绝大多数场景,但是也有其不足,因此有了Etag作为弥补,注意是弥补,不是替代。++
2.协商缓存-Etag
与If-None-Match
为了弥补last-modified
通过修改时间判断的不足,从HTTP 1.1
规范开始新增了一个ETag的头信息,即实体标签(Entity Tag)。
服务器理由资源内容进行哈希计算,生成以一个字符串,类似于文件指纹,只要文件内容编码存在差异,对应的
Etag
就会不同,因此可以使用ETag
对资源进行更精准的感知。
原理:服务器生成etag
后,首次请求的响应会在在响应头中加入etag
属性,再次请求的时候,浏览器在请求头中带上If-Node-Match
属性,值就是上一次响应的Etag
属性值,服务器收到请求后,对比If-None-Match
与文件etag
的值,一致就命中缓存,返回304
,反之,返回新的资源,响应200
。
在nodejs中,生成ETag
的方式可以借助一个第三方库etag,也可以用Crypto
模块根据文件内容生成MD5
,SHA1
等。
nodejs中实现基于ETag
的协商缓存示例
js
const fs = require("fs");
const etag = require("etag"); // 第三方库
/*......*/
const data = fs.readFileSync("./3.jpg");
const contentEtag = etag(data); // 根据文件内容生成etag
const ifNoneMatch = req.headers["if-none-match"];
if (ifNoneMatch === contentEtag) {
// 命中协商缓存
res.statusCode = 304;
res.end();
return;
}
// 没命中缓存
res.writeHead(200, {
"cache-control": "no-cache", // 必须设置no-cache才是协商缓存
"etag":contentEtag, // etag
});
res.end(data);
return;
/*......*/
ETag
的不足:
- 服务器根据文件内容生成
ETag
需要付出额外的计算开销,有可能会影响性能,特别是大文件,数量较多时候。 - ETag分为强验证和若验证,强验证就是根据资源内容进行生成,可以保证每一个字节都相同,弱验证则是根据部分属性值来生成,速度快但是无法保证每个字节都相同,并且在服务器集群的场景下,也会因为不够准确而降低协商缓存有效性验证的成功率。
缓存决策(正确使用)
1.决策前分析
理想情况:我们希望资源在客户端上尽可能久的保存,同时又希望能在资源被修改时及时更新。
现实情况:两种诉求是互斥的,使用强缓存并定义足够长的过期时间就能在客户端缓存长期驻留,但由于强缓存的优先级高于协商缓存,所以很难进行及时更新,若使用协商缓存,虽然能保证及时更新,但频繁与服务器进行协商验证的响应速度肯定不及使用强缓存快。
2.缓存决策树
在面对一个具体资源缓存时,到底该如何指定缓存策略?可以参照如下的缓存决策树来逐步确定对一个资源具体的缓存策略。
3.最终方案(参考)
- 先对一个项目种的文件进行分类,一般分为
html
、css
、javascript
、图片字体等静态文件
。 HTML
文件:进行协商缓存(cache-control:no-cache
),因为一般情况下HTML
是承载了其他文件(css
、javascript
)的主文件。图片或字体
文件:设置强缓存,因为图片一般要修改都是替换性修改,字体也一样。所以采用强缓存,强缓存的时间不宜过长,例如max-age=86400
(秒)css
样式表:设置强缓存,可以在打包时候,在文件名种加入一个根据内容生成的hash签名,这样在每次文件变化后,请求的url都不一致,因此必然会进行重新请求,,同时考虑网络中浏览器与CDN等中间代理的缓存,其强缓存过期时间可以适当延长到一年,即cache-control: max-age=31536000
javascript文件
:设置强缓存,同css一样,若js中含有用户自认信息不想被中间代理缓存,可以cache-control
添加private
属性值
4.缓存设置注意事项
- 拆分源码,分包加载 缓存不是万能的,若资源文件很大,第一次加载就会很慢。
- 预估资源的缓存时效 根据不同资源的不同需求的特点,规划相应的缓存更新时效,为强缓存指定合适的
max-age
值,为协商缓存提供验证更新的ETag
。 - 控制中间代理的缓存 凡是会涉及到隐私的信息尽量避免中间代理的缓存,如果对所有用户响应相同的资源,则可以考虑让中间代理也进行缓存。
- 避免网址的冗余 缓存是根据请求资源的URL进行的,不同的资源会有不同的URL,所以尽量不要将相同的资源设置为不同的URL.
- 规划缓存的层次结构 参考最终方案,不仅仅是请求的资源类型,文件资源的层次结构也会对定制缓存决策略有一定影响。我们应当综合来考虑。