iT邦幫忙

2021 iThome 鐵人賽

DAY 27
1
Modern Web

今晚,我想來點 Web 前端效能優化大補帖!系列 第 27

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


上一篇
Day26 X Memory Management In JavaScript
下一篇
Day28 X Runtime Performance Debugging
系列文
今晚,我想來點 Web 前端效能優化大補帖!30

尚未有邦友留言

立即登入留言