浏览器缓存

444 阅读8分钟

什么是缓存

缓存对应的单词是cache。计算机中很多地方都涉及缓存。我理解的缓存是:用空间换时间

生活中的例子

遇到生词查字典获得读音。

  1. 输入:
  2. 查字典
  • 确定它的部首
  • 找到部首所在的页码
  • 进入页码,数下除去部首之外的笔画数
  • 翻到对应的笔画数位置,
  • 找到字及页码
  • 翻到对应的页码
  • 找到字
  1. 返回 huan

那如果下次再遇到 这个字呢?再查一遍么?肯定不要~~

优化之后的流程是这样的:

  1. 查生字
  2. 小卡片中找
  • 找不到,查字典,并将结果记录在小卡片
  • 找到了,直接返回,不用查字典哪

上面的小卡片就是缓存

它的基本思路是 空间换时间,提升效率。

浏览器缓存

浏览器缓存:浏览器根据特殊的响应头(后端设置)自动做的缓存,与前端基本无关。

浏览器在请求网络资源时,也会做缓存:把请求到的资源在本地(或者内存)保存一份,以便下次直接读取。

以win10+chrome浏览器为例,它的缓存文件保存在:C:\Users\用户名\AppData\Local\Google\Chrome\User Data\Default\Cache目录下,

这些文件是二进制格式的,无法直接打开看。可以借用ChromeCacheView来具体查看。

image.png

浏览器缓存的分类

  1. 强缓存
  2. 协商缓存

image.png

图示地址:www.processon.com/view/link/6…

强缓存

第一次请求成功之后,服务器在响应请求的同时会设置特殊的响应头来和浏览器约定过期时间:下次请求在过期时间之内的,就不发出请求了,直接使用缓存。

这个过程由浏览器自动完成

整体流程

强缓存整体流程:

  1. 浏览器第一次发请求(没有任何缓存), 服务器收到请求之后:
    • 设置特殊响应头(http1.0和1.1有不同的设置),来约定缓存过期时间
    • 返回数据
  2. 浏览器收到响应。保留本次请求的结果到缓存区,并记下过期时间
  3. 浏览器第二次发请求,检查过期时间:
    • 没有过期,直接使用缓存数据
    • 已经过期,流程向后走

Expires(http 1.0)

在 http 1.0 版本中,强制缓存通过 Expires 响应头来实现。

格式

// nodejs代码
const expires = new Date()
expires.setTime(expires.getTime() + 10 * 1000)
// 设置过期时间是:以服务器上的时间为基准的10秒之后的时间
res.setHeader('Expires', expires.toUTCString()) 
// UTC时间比中国北京时间早8小时

假设第1次请求本资源时(/somegood),服务器上的时间是:2022-01-13 12:00:00,那上面的代码表示的是浏览器在2022-01-13 12:00:10之前再次请求/somegood,都会命中缓存。

在network中看到的效果就是:

image.png

注意

  1. 可能显示from memory or from disk,两种方式根据浏览器的策略而定。
  2. 是否过期的判断是以浏览器上的时间为准的,由于服务器和客户端的时间可能不一致,所以判断可能会存在偏差。

Cache-Control(http 1.1)

在 http 1.1 版本中,强制缓存通过 Cache-Control 响应头来实现。它的取值如下:

private:客户端可以缓存
public:客户端和代理服务器均可缓存
max-age=xxx:缓存的资源将在 xxx 秒后过期
no-cache:需要使用协商缓存来验证是否过期
no-store:不可缓存

格式

// 过期时间是相对时间:就是5秒
res.setHeader('Cache-Control', 'public, max-age=5')

最常用的字段就是 max-age=xxx ,表示缓存的资源将在 xxx 秒后过期。这样就可以避免服务器和客户端时间不一致的问题。

强缓存总结

  • 强制缓存只有首次请求才会跟服务器通信
  • 在缓存有效期内,读取缓存资源时不会发出任何请求。从network中看,请求的 Status 状态码为 200,资源的 Size 为 from memory 或者 from disk
  • 在http1.0和1.1中的响应头不一样。为了兼容,一般会同时设置两个响应头
  • 1.1版本的实现优先级会高于 http 1.0 版本的实现

协商缓存

协商缓存,顾名思义,本次请求是否缓存,需要浏览器和服务器协商来定。

与强制缓存的不同之处在于,协商缓存每次请求时都需要携带缓存标识将请求发到服务器。

流程

协商缓存流程

  1. 在第一次请求服务器时,服务器会返回资源,并且返回一个资源的缓存标识(特殊的响应头),一起存到浏览器的缓存数据库。
  2. 第二次请求时:
    • 浏览器会自动将携带缓存标识(特殊的请求头)发送给服务器
    • 服务器拿到标识后判断标识是否匹配
      • 缓存标识不匹配,表示请求的资源有更新
        • 服务器将新数据和新缓存标识一起返回到浏览器
        • 浏览器展示新数据,并自动保存新的缓存标识
      • 缓存标识匹配,表示资源没有更新
        • 服务器直接设置 304 状态码,结束请求
        • 浏览器收到304,就读取本地缓存中的数据。

在 http 协议 1.0 和 1.1 版本中通过不同的响应头来实现。

Last-Modified(http 1.0)

Last-Modified

在 http 1.0 版本中,第一次请求资源时服务器通过 设置Last-Modified 响应头来做缓存标识,并且把资源最后修改的时间作为值填入,然后将资源返回给浏览器。

格式

// 设置响应头
// 1. 不要强缓存
res.setHeader('Cache-Control', 'no-cache')
// 2. 设置响应Last-Modified
const lastModified = 资源最近更新时间.toUTCString()
res.setHeader('Last-Modified', lastModified)

在第二次请求时,浏览器会首先带上 If-Modified-Since 请求头去访问服务器。

image.png

服务器会将 If-Modified-Since 中携带的时间与资源当前修改的时间做匹配:

  • 时间不一致,表示资源有更新。服务器会重新处理请求逻辑,并返回新的资源,同时更新Last-Modified 值,添加在响应头中,带给浏览器
  • 时间一致,表示资源没有更新,服务器直接返回 304 状态码给浏览器,(浏览器白跑一趟,拿了个寂寞),浏览器收到304后,从本地缓存数据库中读取缓存资源。

注意

这种方式有一个弊端:最近修改时间变了,并不能表示资源本身变了,会给判断带来误差。

例如:当服务器中的资源增加了一个字符,后来又把这个字符删掉。这个操作后,资源文件并没有发生变化,但修改时间却发生了变化。当下次请求过来时,服务器也会把这个本来没有变化的资源重新返回给浏览器,导致了不必要的更新。

Etag(http 1.1)

在 http 1.1 版本中,服务器通过 在响应头中补充Etag字段来补充缓存标识。

Etag的值由服务端自行生成,表示当前资源文件的一个唯一标识。这个标识符生成策略由服务器端自己来定。一般有两种:

  1. 根据文件内容和字符长度来生成
  2. 根据使用文件大小和修改时间生成 很显然,策略不同,性能开销就不同。

参考etag

格式

res.setHeader('ETag', 服务器根据本次请求的资源计算生成的标识)

流程

  1. 在第1次请求时(没有缓存)服务器会将资源和 Etag 一起返回给浏览器,浏览器自动做缓存
  2. 在第2次请求时,浏览器会自动将 Etag 信息放到请求头的If-None-Match中去访问服务器,服务器收到请求后,会将服务器上当前的文件标识与请求头中的Etag进行对比:
  • 如果不相同,服务器会重新处理请求逻辑,并返回新的资源,同时更新Etag 值,添加在响应头中,带给浏览器
  • 如果相同,服务器返回 304 状态码

协商缓存总结

  • 协商缓存每次请求都会与服务器交互:
    • 第1次是拿到资源和缓存标识
    • 从第2次开始,就是浏览器询问服务器资源是否有更新的过程。每次请求都会自动携带请求头字段,如果命中缓存,则资源的 Status 状态码为 304
  • 在http1.0和1.1中的响应头/请求头 不一样,分别是 Last-Modified/If-Modified-Since,和 Etag/If-None-Match
  • 一般来讲,为了兼容,两个版本的响应头会同时设置,此时,http 1.1 版本的实现优先级会高于 http 1.0 版本的实现。

最佳实践

原则1. 尽可能多更多地使用本地的资源,减少请求,给用户更快的打开速度,同时也减轻服务器压力。

原则2. 项目更新版本的时候让客户端的缓存失效,能访问到新的资源。

在前端工程化开发的背景下(SPA + webpack打包工具),最佳实践:

  1. 将文件名带hash值(webpack打包给的)的静态资源(样式,JS,图片等)设置为强缓存
  2. 将index.html使用协商缓存

总结

  1. 处理浏览器缓存,前端er无需做任何代码层面的工作,都是由服务器端设置的
  2. 策略有两种:强缓存和协商缓存
  3. 本质上就是特殊的响应头和自动携带的请求头
  4. 这里有一份简单的demo代码