Day27 X Stale While Revalidate Cache Policy

在 Day24 介绍 Web 前端渲染架构时,有提到 Stale While Revalidate 这个快取的应用策略,今天将更详细介绍这个策略,并看看怎麽运用它来搭配前端常见的 API Data Fetching。

应该有读者会注意到这个主题好像放错地方了,应该要放到快取章节才对。没错!因为後来临时更换主题,但是铁人赛发过的文章就像变了心的女朋友一样回不来了,因此只能把此篇文章放到这个章节中,请各位读者见谅XDD

HTTP stale-while-revalidate

stale-while-revalidate 其实源自於 HTTP Cache-Control header 的一个属性 (这个属性在 Day17 时没有特别去讲到)

Cache-Control: max-age=1, stale-while-revalidate=59<秒数>

它的主要概念为:当第一次发出 request 时,浏览器会将回传的资料存到快取里,当之後又有相同的 request 时,浏览器会优先返回快取的版本,让使用者可以迅速得到资料或是看到画面(使用者体验 ++),并在 background 验证快取的资料是不是已经过期,如果需要更新就会抓取最新资料并更新快取,当下次又有请求时就可以拿到刚刚更新过并存到快取的资料。

以上面的例子来说,在第一次请求後,在 1 秒内的其他请求都会直接从快取取得资料,不会做任何 revalidation,而在 1-60 秒的区间如果有请求发生,除了回传快取版本以外,还会在「background」去 revalidate 快取的资料,并在资料有更动时更新快取。而如果是超过 1 分钟後的请求,就会直接以同步的方式发起网路请求抓取最新资料。

这麽做的好处有因为快取机制页面的载入速度会很快,同时也能控制不会得到过於老旧的页面或资料,可以说是在效能与资料新鲜度之间做一个平衡。

更多资讯可以参考这篇文章

stale-while-revalidate for API data fetching

如果你曾经接触过 react-query 或是 SWR 等套件,应该会知道它们其实就是实现 stale-while-revalidate 来做 data fetching 的管理,实践方式是在程序 memory 中管理 cache。

也许你会觉得很奇怪,既然 HTTP caching 的 cache-control 可以做到 stale-while-revalidate 并交给浏览器来管理,为什麽还需要透过这些套件在程序的 memory 中再维护一份 cache 呢?

这个问题问得很好(根本是我自己问的XDD),原因在於 HTTP Caching 的方式比较适合管理静态的资源 (static content),至於动态的 API,要得出一个合理的 max-age 与 stale-while-revalidate 的时间是一个不容易的问题,也因为 API 资料经常变动的特性,其实不使用快取也许会更适合。

如果真的针对 dynamic data 使用 HTTP 的 stale-while-revalidate,我们只能得到 cache data 或是重新抓取的 fresh data 其中之一,但这对於前端应用来说并不是最好的状况,因为我们会希望使用者看到的资料尽量都是最新的,但是等待服务器重新回应新的资料代表着增加页面延迟的可能性,那我们还有没有更好的做法?其实在前端程序里,例如使用 React 搭配 React hooks,我们可以做到更多事情。

以下是我们的策略:

  • 当第一次发出 API 请求时,将 response cache 起来
  • 当之後有请求且可以在快取找到资料时,「立即」回传快取的版本,并在 background 非同步发起请求,并在资料回来时更新画面与快取。

这种方式可以避免在抓取新资料时使用者只能看到一片空白或是 loading state,而是可以看到之前被 cache 的旧资料,让使用者体验更好,并在新资料回传时 rerender,达到希望资料是最新的需求。

Simple Demo Using React & Hooks

首先我得先说,如果在专案中有类似的需求,建议使用 SWR, React-Query, RTK Query 等完整的相关解决方案,以下的 demo 只是为了让读者了解基本运作方式,实际上很多眉眉角角都是不够完整的喔!

首先先用 create-react-app 或其他工具建置一个 react 专案

Demo 要做的是去串接 Pokemon API 并以列表的形式把神奇宝贝的名字显示出来,画面大概是这个样子

首先看一下这个 Pokemon API 的形式

https://pokeapi.co/api/v2/pokemon?offset=${offset}&limit=${limit}

limit 代表一次要拿取的数量,为了方便在画面上看出效果,我选择 limit 为 20,而 offset 则代表从哪一个 item 开始往後数 limit(20) 数量,例如 offset 为 0 得到的就会是第 1-20 的 pokemon 资料,如果 offset 为 20 代表会拿到第 21-40 的资料,为了方便 demo,会设置一个 change offset 的 button,让 offset 在 0 与 20 间切换,换句话说就是会拿到两种不同的资料集。

另外为了模拟复杂系统串接资料时的延迟,我在 data fetching 的地方特别设定了随机的延迟时间。而因为两种 offset 拿回来的资料会长一样,为模拟真实系统中 API data 迅速变化的特性,我会在每一次随机产生一个字串丢到 data list 里。

如果只是这样子的话应该非常简单,我想大家应该早就会了。

从上面的动图会发现一个问题,点选 change offset 後会重新抓一次 API,但得等到资料回传後才会刷新页面,使用者体验不是很好,万一延迟的时间更久,使用者可能会认为网站失去反应而直接离开。

要优化这个使用者体验,我们可以使用 React Hooks 实作一个简单的 stale-while-revalidate 机制来改善它。

直接进入重点,建立一个 useStalewhileRevalidate.js 的 Custom Hooks (这边就不解释 React Hooks 的概念罗!)

重点为在 hooks 外面建立一个物件当作 In Memory 的 Local Cache,把要丢进 fetch 函式的参数与函式的名称做 hash 当作 local object 的 key,每次要重新发起请求前都会先检查 Cache 内有没有针对同一个 key 的资料,有的话就利用旧的资料丢进 React 的 setState 触发 re-render,re-render 之後使用者就可以先看到之前 cache 的资料,这时候再发起 API request,等新的资料回来後先更新快取内同一个 key 的 value,再使用 setState 强迫页面 re-render,这时候使用者看到的页面就会被更新成新的资料。

依照前面的说明,使用者点击 Change Offset 之後,应该会马上看到 cache 的结果,因为每次跑 API 都会 push 一个随机名称的 Pokemon 到阵列里,所以当更新快取且 re-render 後可以看到列表的最後一项被更新了。

刚刚 customized hooks 的程序码中其实还有一些小细节,例如在第一次发出请求前快取是没有资料的,所以做了一个 loading state 让一次发出请求时使用者可以看到 loading 的画面而不是一片空白。再来 hooks 中有一个参数是要丢进 fetcher function 的参数阵列,因为是阵列,所以每次父层 re-render 时都会丢一个新的 reference 进来,所以程序中使用 Ref 来在 lifecycle 中保存 args,并使用 deep compare 来比较传进来的 args props 实质上有没有改变,避免不必要的重新渲染。

不知道各位读者有没有觉得这样的使用者体验变好了呢?

Demo Source Code: https://github.com/kylemocode/it-ironman-2021/tree/master/stale-while-revalidate-demo

本日小结

最近像 SWR, React-Query, RTK Query 这样协助管理 data fetching 与 caching 的工具越来越流行了,在比较大型的前端专案中,免不了会使用 centralized 的 data store 来管理整个 App 的状态,例如 Redux, Recoil...等等。使用这些协助管理 data fetching 与 caching 的工具可以让我们将页面的 state (元件状态、使用者流程状态...等等)与 API data fetching 相关的 state 分开,让我们更好管理与维护应用的 state,同时也能利用 stale-while-revalidate 机制带来的使用者体验提升,在效能与资料新鲜度取得一个不错的平衡。

明天我们将来看看如何使用 Chrome Devtool 的 Performance Tab 来 debug 网站的 runtime performance,铁人赛接近尾声了,希望读者可以再与我坚持几天,明天见罗!

/* 2021/10/24 更新 */

附上我後来写的一篇关於 SWR source code 解析的文章,相信看完会对 stale-while-revalidate 在前端的应用会更加深刻!

References & 图片来源

https://web.dev/stale-while-revalidate/

https://www.toptal.com/react-hooks/stale-while-revalidate


<<:  [Day28]程序菜鸟自学C++资料结构演算法 – 基数排序法(Radix sort)

>>:  [第二十七只羊] 迷雾森林舞会XVI 整理客厅,首页列表介面

我们的基因体时代-AI, Data和生物资讯 Day13- 最基本的生物资讯资料格式Fasta

上一篇我们的基因体时代-AI, Data和生物资讯 Day12-基因疗法中之腺病毒载体与机器学习 我...

Day 08:分治法与递回(1)

再继续写其他更快的排序演算法之前,先来写分治法(divide-and-conquer paradig...

Outlook 2007常见问题 - 整理常被问到的案件

PST档单一档案20GB 超过或将近 , 造成开启无反应或者需时很久. 解决1:scanpst 修...

Day19 网页的页首header

今天我们就要着手开始实作拉!而造顺序来的话我们最上方都会有个logo跟导览列,接下来就让我们把学过的...

Day 10: Creational patterns - Singleton

目的 建立一个「唯一」物件,专责於服务只能单一连线的情境,例如跟资料库的沟通,同时确保全域内都可以呼...