導覽、快取與「不要每次都重刷」
狀態放 URL、上一頁要能回去、資料要有快取層級、捲動位置要記得,以及預抓怎麼做才不浪費。
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.searchParams 或 params 變了才會重跑。所以不要在 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-cache 和 no-store 不一樣:no-cache 是「可以存但每次要問伺服器」,no-store 是「完全不要存」。個資與下載用 no-store。
8. 什麼時候該重新抓
| 情境 | 做法 |
|---|---|
| 使用者按重新整理 | 全部重抓(本來就會) |
| 從別的分頁切回來 | 資料超過 N 分鐘才重抓(visibilitychange) |
| 送出表單之後 | 只失效受影響的 key |
| 每 N 秒輪詢 | 分頁在背景時要停,否則白燒 |
| WebSocket 推播 | 只更新收到的那一筆 |
9. 檢查清單
- 篩選、分頁、排序都在 URL 裡
- 篩選用
replaceState,不會塞爆歷史 - 打字篩選不會讓輸入框失焦、不會跳回頂端
- 沒有
window.location.href做站內導覽 - 上一頁回得去,捲動位置與資料都在
- 連結有預抓
- 樂觀更新失敗時會還原並且告知
- 快取標頭依內容類型設定
- 背景分頁不輪詢