跳到主要内容

缓存控制中的SWR

缓存控制中的 stale-while-revalidate (SWR

stale-while-revalidate 是 HTTP RFC 5861 中描述的一种 Cache-Control 扩展,属于对缓存到期的控制。

stale-while-revalidate=<seconds>。它表明客户端愿意接受陈旧 (stale) 的响应,同时在后台异步检查新的响应 (revalidate)。秒值指示客户愿意接受陈旧响应的时间长度。

怎么样的响应算是陈旧的响应?

stale-while-revalidate 指令应当与 max-age 配合使用,超过 max-age 指定的时间的响应就是陈旧的响应。与之相对的,没有超过时间的就是新鲜的 (fresh) 响应。如果一个缓存的响应没有超过 max-age 指定的时间(仍是新鲜的),按照上面讲的缓存机制,此时请求这个资源,浏览器会直接返回缓存的结果。

如果缓存的结果已经陈旧了呢?

按照前面讲的缓存机制,浏览器应该去请求新的响应了,但是如果存在 stale-while-revalidate 指令就不一样了,浏览器会检查这个陈旧的响应是否超过了 stale-while-revalidate 规定的时间窗口。

如果没有超过,那么浏览器仍然会直接返回缓存的结果,同时在后台请求新的结果用来更新缓存。

用法

Cache-Control: max-age=600, stale-while-revalidate=30

这个响应头规定了缓存的周期为 600 秒,可接受 30 秒内的陈旧响应。如果在 600 秒之内请求这个资源,浏览器就会直接返回缓存的响应。如果在 600 秒之后请求,浏览器会检查是否已经超过可接受的陈旧时间,也就是总共是否超过了 630 秒。如果没有超过的话,仍然返回缓存的响应,同时在后台请求新的响应。如果超过了 630 秒,就直接请求新的响应,应用将等待这个请求。

这和直接设置 max-age=630 有什么不一样?

设置 max-age=630

  • 初次请求,应用等待请求,得到新鲜的响应,存入缓存
  • 在 600 秒内再次请求,不用等,得到缓存响应
  • 在 610 秒时再次请求,不用等,得到缓存响应
  • 在 640 秒时再次请求,应用等待请求,得到新鲜的响应,存入缓存

设置 max-age=600, stale-while-revalidate=30

  • 初次请求,应用等待请求,得到新鲜响应,存入缓存
  • 在 600 秒内再次请求,不用等,得到缓存响应
  • 在 610 秒时再次请求,不用等,得到缓存响应,同时后台请求了新的响应,存入缓存
  • 在 640 秒时再次请求,不用等,得到 610 秒时刷新的缓存响应

可以看到我们在 640 秒时的这个请求,即不用等,也保证了新鲜度。实际上,我们在超过 max-age 的周期之后,在 stale-while-revalidate 指定的时间窗口之内发出的请求,都会得到这个作用。

总结

用一句话来说就是,swr 给你的资源缓存判了「死缓」,虽然缓存过期了,但还能再撑一下(用过期数据返回),下次请求又是一条好汉(上次请求时已经后台刷新过)。

stale-while-revalidate 比较适用的场景是,我们查询的信息需要被刷新,但一定程度的陈旧是可以接受的。通常来讲,这种场景对应的业务是,我们请求的会资源在已知的或者可预见的周期内定时更新,同时我们会多次请求这个资源。在这样的场景下,stale-while-revalidate 可以在提供新鲜度有保证的响应结果的同时,减少重复请求的等待时间。

swr hooks库

https://github.com/vercel/swr

SWR这个名称来自于延时重新验证,这是一种由HTTP RFC 5861推广的缓存失效策略。SWR首先从缓存(过期)返回数据,然后发送fetch请求(重新验证),最后再次提供最新的数据。

swr 即 stale-while-revalidate 的缩写,虽然得名于此,swr 只是借用了它的思想,实际实现与 stale-while-revalidate 指令并无关系。内部应该根据Map和hash来进行的一些个性化缓存处理。

这个库还支持依赖请求,如 B 依赖 A 的返回结果作为请求参数,通常的写法如下:

const { data: a } = await fetch('/api/a')
const { data: b } = await fetch(`/api/b?id=${a.id}`)

那么在 useFetch 的模式下该如何处理这类需求,当 /api/a 接口未正常返回结果时 a 的值为 undefined ,在 /api/b 接口中调用 a.id 就会直接抛出异常,导致页面渲染失败。

那这是否意味我们可以假设当调用接口时 url 这个参数抛出异常,也就意味着它的依赖还没有准备就绪,暂停这个数据的请求;等到依赖项准备就绪时,然后对就绪的数据发起新的一轮请求,以此来解决依赖请求的问题。

而依赖项准备就绪的时机也就是在任一请求完成时,如上面的 /api/a 请求完成时 useFetch 会通过 setState 触发重新渲染,同时 /api/b?id=${a.id} 得到更新,只需要将该 url 作为 useEffect 的依赖项即可自动监听并触发新一轮的请求。