聊聊前端 web 缓存

488 阅读12分钟

web 缓存概念

web 缓存主要指的是两部分:浏览器缓存http 缓存。其中 http 缓存是 web 缓存的核心

浏览器缓存

比如,localStorage,sessionStorage,cookie 等等。这些功能主要用于缓存一些必要的数据,比如用户信息。比如需要携带到后端的参数。亦或者是一些列表数据等等 localStorage,sessionStorage 这种用户缓存数据的功能,他只能保存 5M 左右的数据,多了不行。cookie 则更少,大概只能有 4kb 的数据

http 缓存

http 缓存指的是:当客户端向服务器请求资源时,会先抵达浏览器缓存,如果浏览器有“要求请求资源”的副本,就可以直接从浏览器缓存中提取而不是从原始服务器中提取这个资源。 常见的 http 缓存只能缓存 get 请求响应的资源,对于其他类型的响应则无能为力,所以后续说的请求缓存都是指 GET 请求。 http 缓存都是从第二次请求开始的。第一次请求资源时,服务器返回资源,并在 response header 头中回传资源参数;第二次请求时,浏览器判断这些请求参数,命中强缓存就直接 200,否则就把请求参数加到 request header 头中传给服务器,看是否命中协商缓存,命中则返回 304,否则服务器会返回新的资源。

服务器需要处理 http 的请求,并且 http 去传输数据,需要带宽,带宽是要钱买的。而我们缓存,就是为了让服务器不去处理这个请求,客户端也可以拿到数据。

注意,我们的缓存主要是针对 html,css,img 等静态资源,常规情况下,我们不会去缓存一些动态资源,因为缓存动态资源的易话,数据的实时性就不会不太好,所以我们一般都只会去缓存一些不太容易被改变的静态资源。

::: tip

  • 根据是否需要重新向服务器发起请求来分类,可分为(强制缓存,协商缓存)根据是否可以被单个或多个用户使用来分类,可分为(私有缓存,共享缓存)强制缓存如果生效,不需要再和服务器发生交互,而协商缓存不管是否生效,都需要与服务端发生交互
  • 通常,客户端缓存指的是浏览器缓存,更具体一点,也就是 HTTP 缓存 :::

强制缓存

强制缓存,我们简称强缓存。从强制缓存的角度触发,如果浏览器判断请求的目标资源有效命中强缓存,如果命中,则可以直接从内存中读取目标资源,无需与服务器做任何通讯

  • 基于 Expires 字段实现的强缓存(Expires 字段的作用是,设定一个强缓存时间。在此时间范围内,则从内存(或磁盘)中读取缓存返回。)【Expires 已经被废弃了】
  • 基于 Cache-control 实现的强缓存(代替 Expires 的强缓存实现方法)
// Cache-control这个字段在http1.1中被增加
// nodejs 往响应头中写入需要缓存的时间
// 从该资源第一次返回的时候开始,往后的10秒钟内如果该资源被再次请求,则从缓存中读取,不与服务器做任何交互
res.writeHead(200, {
  'Cache-Control': 'max-age=10',
})

Cache-control 有 max-age、s-maxage、no-cache、no-store、private、public 这六个属性

  • max-age 决定客户端资源被缓存多久。
  • s-maxage 决定代理服务器缓存的时长。
  • no-cache 表示是强制进行协商缓存。
  • no-store 是表示禁止任何缓存策略。
  • public 表示资源即可以被浏览器缓存也可以被代理服务器缓存。
  • private 表示资源只能被浏览器缓存

::: warning

  • 因为 Expires 判断强缓存是否过期的机制是:获取本地时间戳,并对先前拿到的资源文件中的 Expires 字段的时间做比较。来判断是否需要对服务器发起请求。这里有个漏洞:“如果我本地时间不准怎么处理?”
  • no_cache 是 Cache-control 的一个属性。它并不像字面意思一样禁止缓存,实际上,no-cache 的意思是强制进行协商缓存。如果某一资源的 Cache-control 中设置了 no-cache,那么该资源会直接跳过强缓存的校验,直接去服务器进行协商缓存。而 no-store 就是禁止所有的缓存策略了(no-cache 和 no-store 是一组互斥属性,这两个属性不能同时出现在 Cache-Control 中) :::

协商缓存

当第一次请求时服务器返回的响应头中没有 Cache-Control 和 Expires 或者 Cache-Control 和 Expires 过期还或者它的属性设置为 no-cache 时(即不走强缓存),那么浏览器第二次请求时就回与服务器进行协商,与服务器端对比判断资源是否进行了修改更新。如果服务器的资源没有修改,那么就回返回 304 状态码,告诉浏览器可以使用缓存中的数据,这样就减少了服务器的数据传输压力。如果数据有更新就回返回 200 状态码,服务器就回返回更新后的资源并且将缓存信息一起返回。跟协商缓存相关的 header 头属性有(ETag/If-Not-Match、Last-Modified/If-Modified-Since)请求头和响应头需要成对出现

  • 基于 last-modified 的协商缓存
  • 基础 ETag 的协商缓存

基于 last-modified 的协商缓存实现方式

  • 首先需要在服务器端读出文件修改时间
  • 将读出来的修改时间赋给响应头的 last-modified 字段
  • 最后设置 Cache-control:no-cache

::: tip

  • 请求头中的 If-Modified-Since 就是服务器第一次修改时候给他的时间
  • 那么之后每次对该资源的请求,都会带上 If-Modified-Since 这个字段,而务端就需要拿到这个时间并再次读取该资源的修改时间,让它们两个做一个比对来决定是读取缓存还是返回新的资源 :::

协商缓存缺点:
使用以上方式的协商缓存已经存在两个非常明显的漏洞。这两个漏洞都是基于文件是通过比较修改时间来判断是否更改而产生的。

  • 因为是更具文件修改时间来判断的,所以在文件内容本身不修改的情况下,依然有可能更新文件修改时间(比如修改文件名再改回来),这样就有可能文件内容明明没有修改,但是缓存依然失效了。
  • 当文件在极短时间内完成修改的时候(比如几百毫秒)。因为文件修改时间记录的最小单位是秒,所以如果文件在几百毫秒内完成修改的话,文件修改时间不会改变,这样即使文件内容修改了依然不会返回新的文件。
  • 为了解决上述的这两个问题,从 http1.1 开始新增了一个头信息,ETag(Entity 实体标签)

基于 ETag 的协商缓存

  • ETag 就是将原先协商缓存的比较时间戳的形式修改成了比较文件指纹(文件指纹:根据文件内容计算出的唯一哈希值,文件内容一旦改变则指纹改变)
  • 第一次请求某资源的时候,服务端读取文件并计算出文件指纹,将文件指纹放在响应头的 ETag 字段中跟资源一起返回给客户端。
  • 第二次请求某资源的时候,客户端自动从缓存中读取出上一次服务端返回的 ETag 也就是文件指纹。并赋给请求头的 if-None-Match 字段,让上一次的文件指纹跟随请求一起回到服务端。
  • 服务端拿到请求头中的 is-None-Match 字段值(也就是上一次的文件指纹),并再次读取目标资源并生成文件指纹,两个指纹做对比。如果两个文件指纹完全吻合,说明文件没有被改变,则直接返回 304 状态码和一个空的响应体并 return。如果两个文件指纹不吻合,则说明文件被更改,那么将新的文件指纹重新存储到响应头的 ETag 中并返回给客户端

ETag 缺点

  • ETag 需要计算文件指纹这样意味着,服务端需要更多的计算开销。如果文件尺寸大、数量多、并且计算频繁,那么 ETag 的计算就会影响服务器的性能。显然,ETag 在这样的场景下就不是很适合。
  • ETag 有强验证和弱验证,所谓将强验证,ETag 生成的哈希码深入到每个字节。哪怕文件中只有一个字节改变了,也会生成不同的哈希值,它可以保证文件内容绝对的不变。但是强验证非常消耗计算量。ETag 还有一个弱验证,弱验证是提取文件的部分属性来生成哈希值。因为不必精确到每个字节,所以他的整体速度会比强验证快,但是准确率不高,会降低协商缓存的有效性

强缓存和协商缓存对比

强缓存

  • 定义:强缓存是指在浏览器访问资源时,如果资源在缓存中并且没有过期,则直接使用缓存的资源,而不需要向服务器发送请求。

  • 实现方式:通过 HTTP 响应头中的 Expires 和 Cache-Control 来控制。

    • Expires:指定资源的过期时间(绝对时间)。
    • Cache-Control:提供更细粒度的缓存控制,如 max-age(最大有效时间)、no-cache(不使用缓存)等。
  • 优点:可以减少服务器负担,提高页面加载速度。

  • 缺点:如果缓存的资源发生了变化,用户可能不会及时获取到最新的资源,造成数据不一致。

协商缓存

  • 定义:协商缓存是指在浏览器访问资源时,会向服务器发送请求,服务器根据请求判断资源是否需要更新,如果未更新,服务器会告诉浏览器使用缓存资源。

  • 实现方式:通过 HTTP 响应头中的 Last-Modified 和 ETag 来实现。

    • Last-Modified:指示资源的最后修改时间,浏览器在请求时会带上这个时间进行对比。
    • ETag:是资源的唯一标识符,浏览器在请求时会带上这个标识符进行对比。
  • 优点:能够确保用户获取到最新的资源,避免了强缓存中可能导致的数据不一致问题。

  • 缺点:由于需要与服务器进行交互,相对强缓存来说,性能略有下降。

小结

  • 强缓存:完全依赖于浏览器缓存,当缓存未过期时直接使用,不与服务器交互。
  • 协商缓存:需要与服务器进行交互,确保获取到最新的资源,但相对增加了延迟。

选择使用哪种缓存机制,通常取决于具体的应用场景和资源更新的频率。

哪些文件对应哪些缓存

有哈希值的文件设置强缓存即可,没有哈希值的文件(比如 index.html)设置协商缓存 就是 build 出来后的文件比如一些静态资源 css js img 之类的

缓存的配置

// 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 "public, no-cache";
  }
}

为什么要使用 HTTP 缓存

  • 减少了冗余的数据传输,节省了网费。
  • 缓解了服务器的压力,大大提高了网站的性能
  • 加快了客户端加载网页的速度

总结

  • http 缓存可以减少宽带流量(省钱),加快响应速度(增速)。

  • 关于强缓存,cache-control 是 Expires 的完全替代方案,在可以使用 cache-control 的情况下不要使用 expires

  • 关于协商缓存,etag 并不是 last-modified 的完全替代方案,而是补充方案,具体用哪一个,取决于业务场景。

  • 有些缓存是从磁盘读取,有些缓存是从内存读取,有什么区别?答:从内存读取的缓存更快。

  • 所有带 304 的资源都是协商缓存,所有标注(从内存中读取/从磁盘中读取)的资源都是强缓存

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

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

  • 对于强制缓存,服务器通知浏览器一个缓存时间,在缓存时间内,下次请求,直接用缓存,不在时间内,执行协商缓存策略。

  • 对于协商缓存,将缓存信息中的 Etag 和 Last-Modified 通过请求发送给服务器,由服务器校验,返回 304 状态码时,浏览器直接使用缓存

  • 尽量减少 304 的请求,因为我们知道,协商缓存每次都会与后台服务器进行交互,所以性能上不是很好,从性能上来看尽量多使用强缓存