Skip to content

stale-while-revalidate 缓存策略

stale-while-revalidate (简称SWR) 是 HTTP 的一种缓存策略,是 HTTP 的响应头 cache-control的一个属性值。 一种由 HTTP RFC 5861(opens in a new tab) 推广的 HTTP 缓存失效策略。这种策略首先从缓存中返回数据(过期的),同时发送 fetch 请求(重新验证),最后得到最新数据。

如果你有使用过 NEXTjsSWR 的话,其中都有用到 stale-while-revalidate,比如 nextjs getStaticProps 返回的参数 revalidate 就是设置这个属性。

浏览器 SWR 缓存策略主要分为两部分

  1. 判断响应缓存是否过期。
  2. 重新校验响应数据时效性。

📨 例子

假设有一请求的响应头有如下字段👇🏻

Cache-Control: max-age=600, stale-while-revalidate=30
  • 那么浏览器收到该响应后,会将结果缓存到磁盘中,请求的结果在600s内都是新鲜(stale的反义词)的,如果在600s内发起了相同请求,则直接返回磁盘缓存。
  • 若 600s~630s(max-age + stale-while-revalidate)内发起了相同的请求 ,虽然缓存过期了,但依旧使用该缓存结果;另外同时进行异步 revalidate,即重新验证,相当于浏览器在背后自己发一个请求,响应结果留作下次使用。
  • 在 630s 秒后,发起相同请求时,则将和第一次请求一样,从服务器获取响应结果,并且浏览器并把响应结果缓存起来。

那么使用它的优点是❓❓❓ 在传统的同步更新缓存机制中,一个资源的缓存过期之后,如果想再次使用它,需要先对该缓存进行 revalidate。在 revalidate 执行期间,客户端就得等待,直到 revalidate 请求结束。 而 stale-while-revalidate 可以进行后台缓存刷新,提升加载速度

没有十全十美的东西,自然它也不例外👇🏻 stale-while-revalidate 进行异步缓存更新,因此 revalidate 期间时返回上一次的缓存结果,这也导致会牺牲一次(通常)资源的新鲜性

实操案例

服务端代码

javascript
const http = require('http')
const app = http.createServer()
let firstRequest = true

app.on('request', (req, res) => {
  const { url } = req
  const { host } = req.headers
  const route = new URL(url, `http://${host}`).pathname

  let response = {
    stat: 0,
    data: {}
  }

  switch (route) {
    case '/api/list':
	  // 模拟请求后一次后,响应数据改变
      if(firstRequest) {
        response = {
          stat: 1,
          data: '第一次请求的数据'
        }
        firstRequest = false
      } else {
        response = {
          stat: 1,
          data: '最新的数据'
        }
      }
      break
    default:
      break
  }

  res.setHeader("Access-Control-Allow-Origin", "*")
  res.setHeader('Content-Type', 'application/json; charset=utf-8')
  res.setHeader('Cache-Control', 'max-age=30, stale-while-revalidate=10')
  res.end(JSON.stringify(response))
})

app.listen(8888, () => {
  console.log('webserver running')
})

客户端代码

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
	<meta http-equiv="X-UA-Compatible" content="IE=edge" />
	<meta name="viewport" content="width=device-width, initial-scale=1.0" />
	<title>Document</title>
  </head>
  <body>
	<div>第一次请求的数据:<span id="first"></span></div>
	<div>30s~40s请求的数据:<span id="second"></span></div>
	<div>40s后请求的数据:<span id="third"></span></div>

  <script>
    async function fetchData() {
	  const res = await (await fetch('http://localhost:8888/api/list')).json()
	  return res
	}

	async function render() {
	  const firstRes = await fetchData()
	  document.getElementById('first').innerText = JSON.stringify(firstRes)

	  setTimeout(async () => {
	    // 31秒后自动请求
	    const secondRes = await fetchData()
		document.getElementById('second').innerText = JSON.stringify(secondRes)
	  }, 31000)

	  setTimeout(async () => {
	    // 41秒后自动请求
	    const secondRes = await fetchData()
		document.getElementById('third').innerText = JSON.stringify(thirdRes)
	  }, 41000)
	}
  
	render()
  </script>
</body>
</html>

运行结果👇🏻

第一次请求的数据:{stat: 1,data: '第一次请求的数据'}

30s~40s请求的数据:{stat: 1,data: '第一次请求的数据'}

40s后请求的数据:{stat: 1,data: '最新的数据'}.

Released under the MIT License.