Skip to content

HTTP缓存

缓存

  • 缓存的原理: 是在首次请求之后保存一份请求资源的响应副本,当用户再次发起相同请求后,如果判断缓存命中则拦截请求,将之前存储的副本返回给用户,,从而避免重新向服务器发起网络资源请求

  • 缓存好处: 减少网络带宽的浪费,加快资源响应速度,提高网站性能,从而影响用户使用体验 若用户使用的是按量计费的方式访问网络,可以避免多余的网络请求增加用户的网络流量资费。

缓存分类

1. 缓存技术种类

代理缓存 浏览器缓存 网关缓存 负载均衡器及内容分发网络

2. 缓存分类

共享缓存 指缓存内容可以被多个用户使用。比如:公司内部架设的web代理,代理缓存,网关缓存等 私有缓存 指只能单独被用户使用的缓存。比如:浏览器缓存

Http缓存

Http缓存是前端开发中最长接触的缓存机制之一,可以细分为强缓存协商缓存。二者最大的区别在于判断缓存命中时,浏览器是否要向服务器端进行询问以协商缓存的相关信息,进而判断是否需要就响应内容进行重新请求。

Cache-Control【在HTTP/1.1版本中】

cache-control在缓存中是非常重要的字段,用来指定Response-Request遵循的缓存机制。

1. cache-control作为请求头时

属性名:cache-controlimage.png (1)Cache-Control: no-cache 使用no-cache指令的目的是为了防止从缓存中返回过期的资源。 客户端发送的请求中如果包含 no-cache 指令,则表示 客户端将不会接收缓存的资源。每次请求都是从服务器获取资源,返回304。

(2)Cache-Control: no-store 使用no-store 指令表示请求的资源不会被缓存,下次任何其它请求获取该资源,还是会从服务器获取,返回 200,即资源本身。

2. cache-control作为响应头时

属性名:cache-controlimage.png

  1. Cache-Control: public 当指定使用 public 指令时,为共享缓存,既可以被浏览器缓存,也可以被代理服务器缓存。

  2. Cache-Control: private 若未显示指定publicprivate则默认为private 当指定 private 指令后,响应只能被浏览器缓存,这与 public 指令的行为相反。 若在缓存服务器上设置了private,缓存服务器会对该特定用户提供资源缓存的服务,对于其他用户发送 过来的请求,代理服务器则不会返回缓存。

  3. Cache-Control: no-cache 设置对比缓存(协商缓存) 如果服务器返回的响应中包含 no-cache 指令,每次客户端请求,必需先向服务器确认其有效性,如果资源没有更改,则返回304,若资源更改了则返回新资源。

  4. Cache-Control: no-store 设置不缓存(也可以直接不设置缓存字段,默认就是无缓存) 不对响应的资源进行缓存,即用户下次请求还是返回 200,返回资源本身。

  5. Cache-Control: max-age=604800(单位:秒) 设置强缓存 资源缓存在本地浏览器的相对时间,如果超过该时间,则重新向服务器获取。

  6. max-ages-maxagemax-age属性值会比s-maxage更常用 max-age表示服务器告知浏览器响应资源的过期时长。在一般项目的使用场景中基本够用。 对于大型架构的项目通常会涉及使用各种代理服务器的情况,这就需要考虑缓存在代理服务器上的有效性问题,这便是s-maxage存在的意义 s-maxage表示缓存在代理服务器中的过期时长,且仅当设置了public属性值时才有效

为了兼容性,在http1.0中可以使用expires属性实现,但是expires依赖浏览器本地时间。

强缓存

原理:浏览器判断所请求的目标资源有效命中强缓存,则可以直接从强缓存中返回请求响应,无须与服务器进行任何通信。

在响应头中,与强缓存相关的字段是expirescache-controlimage.png

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.强缓存方案

由上可知,强缓存可以设置expirescache-control:max-age=30来实现,并且cache-control能作为expires的完全替代方案,并拥有其所不具备的一些缓存控制特性,在项目中使用cache-control就够了,目前expires还存在的唯一理由是考虑可用性方面的向下兼容。

协商缓存

原理:在使用本地缓存之前,需要向服务器端发起一次GET请求,与之协商当前浏览器保存的本地缓存是否已经过期。

作用:可以帮助我们解决强缓存下资源不更新的问题

注意:协商缓存必须设置cache-control:no-cache才会生效

1.协商缓存-last-modifiedif-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.协商缓存-EtagIf-None-Match

为了弥补last-modified通过修改时间判断的不足,从HTTP 1.1规范开始新增了一个ETag的头信息,即实体标签(Entity Tag)。

服务器理由资源内容进行哈希计算,生成以一个字符串,类似于文件指纹,只要文件内容编码存在差异,对应的Etag就会不同,因此可以使用ETag对资源进行更精准的感知。

原理:服务器生成etag后,首次请求的响应会在在响应头中加入etag属性,再次请求的时候,浏览器在请求头中带上If-Node-Match属性,值就是上一次响应的Etag属性值,服务器收到请求后,对比If-None-Match与文件etag的值,一致就命中缓存,返回304,反之,返回新的资源,响应200

image.png

在nodejs中,生成ETag的方式可以借助一个第三方库etag,也可以用Crypto模块根据文件内容生成MD5SHA1等。

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.缓存决策树

在面对一个具体资源缓存时,到底该如何指定缓存策略?可以参照如下的缓存决策树来逐步确定对一个资源具体的缓存策略。 image.png

3.最终方案(参考)

  1. 先对一个项目种的文件进行分类,一般分为htmlcssjavascript图片字体等静态文件
  2. HTML文件:进行协商缓存(cache-control:no-cache),因为一般情况下HTML是承载了其他文件(cssjavascript)的主文件。
  3. 图片或字体文件:设置强缓存,因为图片一般要修改都是替换性修改,字体也一样。所以采用强缓存,强缓存的时间不宜过长,例如max-age=86400(秒)
  4. css样式表:设置强缓存,可以在打包时候,在文件名种加入一个根据内容生成的hash签名,这样在每次文件变化后,请求的url都不一致,因此必然会进行重新请求,,同时考虑网络中浏览器与CDN等中间代理的缓存,其强缓存过期时间可以适当延长到一年,即cache-control: max-age=31536000
  5. javascript文件:设置强缓存,同css一样,若js中含有用户自认信息不想被中间代理缓存,可以cache-control添加private属性值

4.缓存设置注意事项

  1. 拆分源码,分包加载 缓存不是万能的,若资源文件很大,第一次加载就会很慢。
  2. 预估资源的缓存时效 根据不同资源的不同需求的特点,规划相应的缓存更新时效,为强缓存指定合适的max-age值,为协商缓存提供验证更新的ETag
  3. 控制中间代理的缓存 凡是会涉及到隐私的信息尽量避免中间代理的缓存,如果对所有用户响应相同的资源,则可以考虑让中间代理也进行缓存。
  4. 避免网址的冗余 缓存是根据请求资源的URL进行的,不同的资源会有不同的URL,所以尽量不要将相同的资源设置为不同的URL.
  5. 规划缓存的层次结构 参考最终方案,不仅仅是请求的资源类型,文件资源的层次结构也会对定制缓存决策略有一定影响。我们应当综合来考虑。