一個被低估的 Cache 屬性#
Stale-While-Revalidate (SWR) 其實是 HTTP Cache-Control 的一個屬性,例如:
Cache-Control: max-age=1, stale-while-revalidate=59它的運作邏輯是:
- 第一次發出請求時,瀏覽器把 Response 存進快取。
- 之後遇到相同的請求,瀏覽器會優先回傳快取版本,讓使用者立即拿到資料;接著在背景去驗證快取是否過期,必要時抓取最新資料並更新。
- 等下一次請求進來時,就能拿到剛剛更新過的版本。
以上面的設定為例:
- 第一次請求後 1 秒內:相同請求都直接走快取,不做任何 revalidate。
- 1 至 60 秒之間:仍然回傳快取版本,但會在「背景」revalidate,有更新就更新快取。
- 超過 1 分鐘:直接以同步方式發網路請求拿最新資料。
這個策略的價值在於同時兼顧「載入速度」與「資料新鮮度」,使用者很快就看得到內容,背景再悄悄把資料更新掉。
為什麼還需要在前端再做一層#
如果你用過 React Query 或 SWR 套件,會發現它們其實就是把 Stale-While-Revalidate (SWR) 搬進前端程式裡,靠 JavaScript memory 管理 cache。但既然 HTTP Cache-Control 已經支援,為何還要重複實作?
主要原因是 HTTP 層的 SWR 適合管理靜態資源;對於變動頻繁、跟使用者互動相關的 API,要設定一個合理的 max-age 與 stale-while-revalidate 時間並不容易,常常乾脆「不快取」反而還比較單純。
但若直接走「不快取,每次都打 API」,使用者每次都得等 loading;如果走「強快取一段時間」,又有可能拿到舊資料。前端真正需要的常常是介於兩者之間的折衷:
- 第一次請求 API 時,把回應存到 in-memory cache。
- 之後遇到相同請求,先「立刻」回傳快取版本讓畫面馬上顯示;同時在背景再打一次 API。
- 新資料回來後,更新 cache 並觸發 re-render。
這種模式可以避免使用者只看到空白或 loading state;先看到舊版資料、再被悄悄替換成最新版,整體體驗會明顯變好。
用 React Hooks 做一個簡化版#
真實專案請優先選擇 SWR、React Query、RTK Query 等成熟工具。下面的範例只是為了把概念講清楚,省略了許多邊界情況。
範例的需求是串 PokeAPI,把神奇寶貝列表顯示在畫面上。API 形式如下:
// e.g. https://pokeapi.co/api/v2/pokemon?offset=0&limit=20
const url = `https://pokeapi.co/api/v2/pokemon?offset=${offset}&limit=${limit}`;limit 控制一次拿幾筆,為了 demo 效果取 20;offset 在 0 與 20 之間切換,模擬「兩種不同資料集」的情境。為了模擬真實系統的延遲與資料變化:
- 在資料抓取邏輯加上隨機的延遲時間。
- 每次回應都隨機 push 一個額外字串到陣列尾端,模擬 API 資料的變化。
如果只是純粹「按下按鈕重抓 API」,使用者要等到 Response 回來才會看到畫面更新。延遲一拉長,使用者很容易誤以為頁面當機而離開。
要改善這個體驗,可以做一個 useStaleWhileRevalidate.js 自訂 Hook,重點如下:
- 在 Hook 外層維護一個物件當作 in-memory 的 local cache。
- 把要送進 fetch 的參數與函式名稱做 hash,當作 cache 的 key。
- 每次發請求前先查 cache:
- 有資料:先用舊資料
setState,畫面立刻 re-render,使用者馬上看到內容。 - 接著仍然發出 API 請求,等新資料回來再更新 cache,並再次
setState觸發 re-render。 - 沒資料:保留 loading state,等 API 回應後再顯示。
- 有資料:先用舊資料
- 因為傳進來的 args 是陣列,每次父層 re-render 都會給新的 reference,所以實作上會用
useRef保存 args,再用 deep compare 判斷是否真的改變,避免多餘的 re-render。
實際操作後可以觀察到:點下按鈕後立刻顯示之前的 cache,接著資料的最後一項被「悄悄」換掉,那就是背景 fetch 完成後的 re-render。整體看起來幾乎沒有 loading 的體感。
Cache API、IndexedDB 與其他選項#
前端要做這種快取不一定只能靠 in-memory 物件。依照需求不同,可以選擇:
- 普通 JavaScript Map / Object:跟元件、Hook 同生命週期,重新整理就消失。
- Cache API:通常和 Service Worker 配合使用,適合放網路請求的 Response。
- IndexedDB:可永久保存結構化資料,適合需要跨 session 共用、容量較大的快取。
實作 Stale-While-Revalidate (SWR) 時要根據資料屬性挑選儲存層;像 PokeAPI 這種純讀取且重新整理就重新抓也沒差的情境,in-memory 就很夠用。
本章重點#
- Stale-While-Revalidate (SWR) 是一種「先回傳快取,背景再更新」的快取策略,源自 HTTP
Cache-Control。 - 對於靜態資源,可以直接交給 HTTP 快取(HTTP Caching)處理。
- 對於變動頻繁的 API,自己在前端實作 SWR 通常更彈性,使用者不必盯著 loading state 等待。
- 在 React 中可以用 Custom Hook 加上 in-memory cache 完成簡化版實作;實務上建議直接使用 SWR、React Query 或 RTK Query。
- 依照資料的生命週期與容量,可選擇 in-memory、Cache API、IndexedDB 等不同儲存層。