likes
comments
collection
share

彻底弄懂前端缓存

作者站长头像
站长
· 阅读数 7

前端缓存

前端缓存,这是一个老生常谈的话题,也常被作为前端面试的一个知识点。今天我们再来总结一下。

分类

前端缓存分为强缓存协商缓存两种。

彻底弄懂前端缓存

强缓存

强缓存主要使用Expires、Cache-Control 两个头字段,两者同时存在 Cache-Control 优先级更高。当命中强缓存的时候,客户端不会再发请求,直接从缓存中读取内容,并返回HTTP状态码200。

Expires

Expires 首部字段是 HTTP/1.0 中定义缓存的字段,其给出了缓存过期的绝对时间,是一个GMT 格式的标准时间。

示例

Expires: Wed, 11 May 2022 03:50:47 GMT

当客户端请求服务器的时候,服务器会返回资源的同时还会带上响应头Expires,表示资源的过期具体时间,如果客户端在过期时间之前再次获取该资源,就不需要再请求我服务器了,可以直接在缓存里面拿。

使用Expires强缓存优点:

  1. 在过期时间以内,为用户省了很多流量。
  2. 减少了服务器重复读取磁盘文件的压力。

使用Expires强缓存缺点:

  1. 缓存过期以后,服务器不管文件有没有变化会再次请求服务器。
  2. 缓存过期时间是一个具体的时间,这个时间依赖于客户端的时间,如果时间不准确或者被改动缓存也会随之受到影响。

Cache-Control

Cache-Control 首部字段是 HTTP/1.1 中定义缓存的字段,其用于控制缓存的行为,可以组合使用多种指令,多个指令之间可以通过 “,” 分隔。常用的指令有:max-age、s-maxage、public/private、no-cache/no store 等。

示例

Cache-Control: max-age:3600, s-maxage=3600, public 
Cache-Control: no-cache

Cache-Control既能出现在请求头又能出现在响应头,其不同的值代表不同的意思,下面我们具体分析一下。

Cache-Control 服务端参数:

  • max-age: 在多少秒内有效,是一个相对时间,这样比Expires具体的时间就更精确了。当为0的时候就是不使用强缓存,类似no-cache。
  • s-maxage: 就是用于表示 cache 服务器上(比如 cache CDN,缓存代理服务器)的缓存的有效时间的,并只对 public 缓存有效。
  • no-cache:不使用本地强缓存,需要使用缓存协商。
  • no-store:直接禁止浏览器缓存数据,每次用户请求该资源,都会向服务器发送一个请求,每次都会下载完整的资源。
  • public:可以被所有的用户缓存,包括终端用户和中间代理服务器。
  • private:只能被终端用户的浏览器缓存,不允许中间缓存代理进行缓存。(这个是默认值)

Cache-Control 客户端参数:

  • no-cache: 在请求首部中被使用时,表示告知(代理)服务器不直接使用缓存,要求向源服务器发起请求。
  • max-stale: 5 表示客户端到代理服务器上拿缓存的时候,即使代理缓存过期了也不要紧,只要过期时间在 5 秒之内,还是可以从代理中获取的。
  • min-fresh: 5 表示代理缓存需要一定的新鲜度,不要等到缓存刚好到期再拿,一定要在到期前 5 秒之前的时间拿,否则拿不到。
  • only-if-cached: 这个字段加上后表示客户端只会接受代理缓存,而不会接受源服务器的响应。如果代理缓存无效,则直接返回 504(Gateway Timeout)。

协商缓存

协商缓存可以看作是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程。

协商缓存主要有四个头字段,它们两两组合配合使用,Last-Modified 和 If-Modified-Since 一组,Etag 和 If-None-Match 一组,当同时存在的时候会以 Etag 和 If-None-Match 为主。当命中协商缓存的时候,服务器会返回HTTP状态码304,让客户端直接从本地缓存里面读取文件。

  • Last-Modified

响应头,资源最近修改时间,由服务器告诉浏览器。

  • If-Modified-Since

请求头,资源最近修改时间,由浏览器告诉服务器。其实就是第一次访问服务端返回的Last-Modified的值。

  • Etag

响应头,资源标识,由服务器告诉浏览器。

  • If-None-Match

请求头,缓存资源标识,由浏览器告诉服务器。其实就是第一次访问服务端返回的Etag的值。

Last-Modified 和 If-Modified-Since

示例

Last-Modified: Fri , 14 May 2021 17:23:13 GMT 
If-Modified-Since: Fri , 14 May 2021 17:23:13 GMT

当客户端第一次请求服务器的时候,服务端会返回一个Last-Modified响应头,该字段是一个标准时间。客户端请求服务器的时候会带上If-Modified-Since请求头字段,该字段的值就是服务器返回的Last-Modified的值。服务器接收到请求后会比较这两个值是否一样,一样就返回304,让客户端从缓存中读取,不一样就会返回状态码200,并返回新文件给客户端并更新Last-Modified响应头字段的值。

使用 Last-Modified 和 If-Modified-Since 的优点:

  • 当缓存有效时服务器不会返回文件给客户端,而是直接返回304状态码,让客户端从缓存中获取文件。大大节省了流量和带宽以及服务器的压力。

使用If-Modified-Since 和 Last-Modified的缺点:

  • Last-Modified 过期时间只能精确到秒。如果在同一秒既修改了文件又获取文件,客户端是获取不到最新文件的。
  • 如果只单纯编辑了下文件,并没有更新内容,Last-Modified会被更新,导致缓存失效。

Etag 和 If-None-Match

示例

Etag: "29322-09SpAhH3nXWd8KIVqB10hSSz66" 
If-None-Match: "29322-09SpAhH3nXWd8KIVqB10hSSz66"

为了解决文件修改时间只能精确到秒和单纯编辑不修改内容也会失效的问题,我们引入 Etag 响应头。Etag 是由文件内容的 hash 值和内容长度计算而成,只有当文件文件内容发生变化了Etag的值才会发生变化。

当客户端第一次请求服务器的时候,服务端会返回一个Etag响应头。客户端请求服务器的时候会带上If-None-Match请求头字段,该字段的值就是服务器返回的Etag的值。服务器接收到请求后会比较这两个值是否一样,一样就返回304,让客户端从缓存中读取,不一样就会返回新文件给客户端并更新Etag响应头字段的值。

使用Etag 和 If-None-Match的优点:

  • 当缓存有效时服务器不会返回文件给客户端,而是直接返回304状态码,让客户端从缓存中获取文件。大大节省了流量和带宽以及服务器的压力。
  • 解决了一秒内修改并读取的问题。
  • 使用弱 eTag能解决单纯编辑不修改内容而导致缓存失效的问题。

弱 ETag 值只适用于提示资源是否相同。只有资源发生了根本改变,产生差异时才会改变 ETag 值。这时会在字段值最开始处附加 W/。

ETag: W/"29322-09SpAhH3nXWd8KIVqB10hSSz66"

扩展

缓存失效问题

引入了缓存固然是好事,能大大提升响应速度以及减轻服务端的压力,但是也会出现一些问题,比如我们明明更新了系统版本,为什么客户端看到的还是老文件。在不同的时代有不同的解决方案。

老方案

老方案通过人工自己修改文件名或者在文件名后带上版本号、时间戳,这样客户端就会当新文件请求并使用,之前的强缓存就算在有效期内也会失效。

<script src="http://randy.js?version=1.1.1> </script>

新方案

在现在的构建阶段基本上都不需要人工操作了,都是使用构建工具比如Wbpack、Gulp、Grunt等构建工具自动构建。比如在使用Webpack构建的时候,就会使用hash值来给文件命名(webpack中有hash、chunkhash、contenthash三种hash值),当内容或文件名发生改变的时候,构建出来的文件名也一定会不一样,这样也解决了强缓存还在有效期内的问题。

pragma

pragma是旧产物,已经逐步抛弃,有些网站为了向下兼容还保留了这个字段。pragma的值为no-cache时,表示禁用缓存。优先级是 pragma > cache-control > expires。

流程图

有了这张流程图,可以让你们理解的更清楚。

彻底弄懂前端缓存

缓存的配置

如果我们使用Nginx作为Web服务器,我们可以如下配置

location / {

  # 其它配置
  ...

  if ($request_uri ~* .*[.](js|css|map|jpg|png|svg|ico)$) {
    #非html缓存1个月
    add_header Cache-Control "public, max-age=2592000";
  }

  if ($request_filename ~* ^.*[.](html|htm)$) {
    #html文件使用协商缓存
    add_header Cache-Control "no-cache";
  }
}

缓存到底存在哪?

很多小伙伴会好奇,这缓存到底存在哪里了呢?别急,我们接着往下讲。

按缓存位置分类我们可以分为memory cache、disk cache、Service Worker三类,我们可以在 Chrome 的开发者工具中,Network -> Size 一列看到一个请求最终的处理方式:如果是大小 (多少 K, 多少 M 等) 就表示是网络请求,否则会列出 from memory cachefrom disk cachefrom ServiceWorker就表示命中了缓存。

  • memory cache 是内存中的缓存,(与之相对 disk cache 就是硬盘上的缓存)。按照操作系统的常理:先读内存,再读硬盘。

彻底弄懂前端缓存

  • disk cache 也叫 HTTP cache,顾名思义是存储在硬盘上的缓存,因此它是持久存储的,是实际存在于文件系统中的。而且它允许相同的资源在跨会话,甚至跨站点的情况下使用,例如两个站点都使用了同一张图片。

彻底弄懂前端缓存

  • 上述的缓存策略以及缓存/读取/失效的动作都是由浏览器内部判断进行的,我们只能设置响应头的某些字段来告诉浏览器,而不能自己操作。service work给予了我们另外一种更加灵活,可以直接的操作方式。我们可以从 Chrome 的 Application找到Service Workers。这个缓存是永久性的,即关闭 TAB 或者浏览器,下次打开依然还在(而 memory cache 不是)。有两种情况会导致这个缓存中的资源被清除:手动调用 API cache.delete(resource) 或者容量超过限制,被浏览器全部清空。

系列文章

详解计算机网络模型

详解TCP和UDP

详解HTTP和HTTPS

后记

感谢小伙伴们的耐心观看,本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!