跳到主要內容

導覽、快取與「不要每次都重刷」

狀態放 URL、上一頁要能回去、資料要有快取層級、捲動位置要記得,以及預抓怎麼做才不浪費。

約 5 分鐘 · navigation-cache.md

1. 篩選狀態放 URL

判準:如果使用者可能想把這個畫面傳給同事,那個狀態就該在網址裡。

放 URL:篩選、搜尋字串、分頁、排序、目前分頁籤、選到哪一筆。 放元件 state:選單開了沒、hover 在哪、拖曳中的位置。

function setParam(key: string, value: string) {
  const url = new URL(page.url);
  if (value) url.searchParams.set(key, value);
  else url.searchParams.delete(key);

  // replaceState:打字篩選不該在歷史裡塞 20 筆
  goto(url, { replaceState: true, keepFocus: true, noScroll: true });
}

三個選項都重要:

  • replaceState — 否則按上一頁要按 20 次才離開這一頁
  • keepFocus — 否則每輸入一個字,焦點就從搜尋框跳走
  • noScroll — 否則每次篩選都跳回頂端

Next.js 對應的是 router.replace(url, { scroll: false }),Vue Router 是 router.replace()

2. 不要整頁重載

<a href> 在 SPA 框架裡通常會被攔截成 client 導覽。會退化成整頁重載的情況:

  • target="_blank"(本來就該重載)
  • 外部網址
  • 手動 window.location.href = ...這是最常見的錯誤
  • 表單 method="GET" 沒攔截

判斷方式:切頁時 header 有沒有閃一下。有,就是整頁重載了。

3. 快取層級

1. 元件內           一次互動的暫存
2. 路由 load 快取   同一頁重進來不用重抓
3. HTTP 快取        瀏覽器與 CDN
4. 服務層           跨頁共用的資料

SvelteKit 的 load 預設會依賴追蹤:只有你用到的 url.searchParamsparams 變了才會重跑。所以不要在 load 裡無意義地讀整個 url

要手動失效:

await invalidate('app:projects');   // 只重跑依賴這個 key 的 load
await invalidateAll();              // 全部重跑,用在語言切換這種全站變動

invalidateAll() 要省著用。 它會重跑每一個 load,包含跟這次改動無關的。

4. 上一頁要能回去

三件事讓上一頁真的可用:

一、捲動位置。 框架通常會處理,但如果你的清單是虛擬捲動或懶載入,就要自己存:

sessionStorage.setItem(`scroll:${key}`, String(container.scrollTop));

二、資料還在。 回到清單頁如果重新 fetch,使用者會看到 loading 再看到內容。清單資料應該在快取裡。

三、表單內容。 使用者填到一半跳去看說明再回來,內容不該消失。長表單用 sessionStorage 暫存(但不要存密碼、信用卡)。

5. 預抓(prefetch)

<body data-sveltekit-preload-data="hover">

滑鼠移到連結上就開始抓,點下去時通常已經好了。這是最划算的效能改善之一。

策略:

  • hover — 預設。桌機好用
  • tap — 行動裝置(touchstart 就開始,比 click 早 ~100ms)
  • off — 昂貴或有副作用的路由

不要對所有連結用 eager,那等於幫使用者下載整個網站。

6. 樂觀更新

按讚、勾選、刪除這類「幾乎一定會成功」的操作,先更新 UI 再送請求:

const previous = items;
items = items.filter((item) => item.id !== id);   // 先更新

try {
  await api.delete(id);
} catch (error) {
  items = previous;                                // 失敗就還原
  showToast('刪除失敗,已經復原');                  // 而且要說
}

失敗一定要說。 靜靜還原比不做樂觀更新更糟 —— 使用者以為刪掉了。

不適合樂觀更新的:付款、不可逆的操作、需要伺服器產生 id 的東西。

7. HTTP 快取

內容 Cache-Control
帶 hash 的靜態資源 public, max-age=31536000, immutable
產生的 CSS(如 themes.css) public, max-age=3600
HTML 頁面 no-cache(可存但每次要驗證)
個人化資料 private, no-store
檔案下載 no-store

no-cacheno-store 不一樣:no-cache 是「可以存但每次要問伺服器」,no-store 是「完全不要存」。個資與下載用 no-store

8. 什麼時候該重新抓

情境 做法
使用者按重新整理 全部重抓(本來就會)
從別的分頁切回來 資料超過 N 分鐘才重抓(visibilitychange
送出表單之後 只失效受影響的 key
每 N 秒輪詢 分頁在背景時要停,否則白燒
WebSocket 推播 只更新收到的那一筆

9. 檢查清單

  • 篩選、分頁、排序都在 URL 裡
  • 篩選用 replaceState,不會塞爆歷史
  • 打字篩選不會讓輸入框失焦、不會跳回頂端
  • 沒有 window.location.href 做站內導覽
  • 上一頁回得去,捲動位置與資料都在
  • 連結有預抓
  • 樂觀更新失敗時會還原並且告知
  • 快取標頭依內容類型設定
  • 背景分頁不輪詢

顯示設定

這裡改的每一項,會即時套用到站上所有預覽。

風格

密度

圓角

動態

系統層級的「減少動態效果」永遠優先於這裡的設定。

語言