昨天我們解決了「這份狀態誰拿得到」的問題——局部狀態留在元件裡,跨頁共用的狀態升級成全域狀態。但升級成全域狀態,不代表它撐得過使用者按下重新整理。只要沒有額外寫進瀏覽器的儲存機制,再全域的狀態一樣活在記憶體裡,重整就歸零。今天要談的,就是三種儲存機制——記憶體快取、SessionStorage、LocalStorage——各自的生命週期,以及該替哪種資料選哪一種。
定義:透過瀏覽器提供的儲存 API,將應用程式的狀態資料暫存於使用者裝置,以達成更快的存取速度與離線友善體驗。
定義:資料存於執行中的程式記憶體(如變數、State),讀取速度最快。
生命週期:隨應用程式重啟(或頁面重新整理)而消失,生命週期最短。
適用:需要即時運算但不需要保存的資料(如:當前畫面的臨時狀態)。
定義:Web Storage API 的一種,資料綁定於特定分頁(Tab)。
生命週期:重新整理頁面不會清除,資料會保留;只有真正關閉分頁或視窗時,資料才會被清除。這是它跟記憶體快取最關鍵的差異——兩者聽起來都「暫時」,但一個撐得過重整,一個撐不過。
適用:僅在該次操作流程中有效的資料(如:單次瀏覽的臨時暫存)。
定義:Web Storage API 的一種,資料綁定於網域(Domain)。
生命週期:永久保存,除非程式明確執行清除(clear、removeItem)或使用者手動清理瀏覽器資料。
適用:跨視窗、跨時間需要持續存在的偏好設定與識別資料。
一個共通的限制:SessionStorage 跟 LocalStorage 都只能存字串。要存物件的話,得先用 JSON.stringify 轉成字串,讀出來再用 JSON.parse 轉回物件,不然存進去的會變成沒用的 "[object Object]":
// LocalStorage:存使用者偏好,重開瀏覽器還在
localStorage.setItem('preferredAddress', JSON.stringify({ city: '台北市', district: '大安區' }));
const saved = JSON.parse(localStorage.getItem('preferredAddress'));
// SessionStorage:存篩選條件,關掉分頁才消失,重新整理還在
sessionStorage.setItem('filters', JSON.stringify({ region: '日韓', priceMin: 1000, priceMax: 5000 }));
以代購 App 為例,這三種機制可以這樣分配:
記憶體快取:用於即時匯率換算結果。因為匯率隨時在變,頁面重新載入後本來就該重新抓一次最新的,不需要特地保存舊的。
SessionStorage:用於「進階篩選條件」。例如使用者在代購列表頁設定了「日韓代購」、「價格區間 $1000–$5000」,切到詳細頁再按上一頁回來時,這些條件應該還在;但關閉分頁後,下次重新進來,篩選條件不需要留著。
LocalStorage:用於使用者偏好設定、歷史搜尋紀錄——以及昨天提到的購物車內容。昨天把購物車定位成 Global Store,解決的是「所有頁面都拿得到」這個問題,但 Global Store 本身還是活在記憶體裡,使用者不小心重整頁面,購物車一樣會清空。要讓購物車撐過重整、甚至撐過使用者關掉瀏覽器隔天再回來,就得把它額外寫進 LocalStorage。這兩天的內容合起來看,才是完整答案:購物車既要是全域可存取的(Day 9 的結構問題),也要是能撐過重新整理的(Day 10 的持久化問題)。
這篇核心在於釐清資料的「保存需求」。記憶體快取追求速度,SessionStorage 關注當次操作流程的連續性,LocalStorage 則負責跨時間的持久化。掌握這三者的生命週期,能有效平衡效能與使用者體驗。這其實是我很久以前、單純以一個使用者的角度就很想問的問題——回到列表頁,篩選條件明明還在;但重新整理,怎麼有時候又整個被清空?以前只覺得這是巧合,或是這個網站做得比較細心,今天才知道背後其實是不同儲存機制在分工:記憶體快取撐不過重新整理,SessionStorage 卻撐得過重新整理,只有真的關掉分頁才會清空。原來我這麼多年當使用者時觀察到的落差,一直都有明確的技術原因,只是我從來不知道要往哪裡找答案。