題目與適用情境
一個電商結帳頁在瀏覽紀錄導覽中出現兩種行為。有時點擊返回後,原本的 DOM、表單值、捲動位置和 JavaScript 堆積幾乎瞬間還原;這份快照可能仍保存舊購物車總額,或另一個分頁已經登出的工作階段。有時點擊返回會建立新文件,重新執行一般載入流程。團隊一直用「瀏覽器快取」概括所有現象,並考慮全域關閉快取。
請解釋往返快取(back/forward cache,簡稱 bfcache)與 HTTP 快取、SPA 路由快取的差異,並設計一套生命週期與排障方案:
- 分開辨識已經確認的 bfcache 還原和一般文件載入;
- 在允許高影響操作前,重新驗證身分、購物車及其他敏感狀態;
- 頁面隱藏時釋放資源,還原後只重新連線一次;
- 找到頁面、子 frame 和第三方程式碼中的資格阻塞項;
- 依瀏覽器版本衡量瀏覽紀錄遍歷、確認還原、未命中和阻塞原因;
- 在符合產品安全政策時保留這項效能收益。
本題適用於資深前端、Web 效能、瀏覽器平台和前端架構面試。它考察候選人能否圍繞「被暫停的文件」推理,而不只會背快取回應標頭。
面試官考察什麼
第一,候選人應畫清三種機制。HTTP 快取保存可重複使用的回應,遵守請求、新鮮度和重新驗證規則。bfcache 可能在記憶體中保存整個跨文件頁面,包括 DOM 與 JavaScript 堆積,並在工作階段瀏覽紀錄遍歷時暫停、還原。用戶端路由器可以在同一份文件內的軟式導覽中保存應用程式資料或 UI。清理其中一種快取,無法診斷另外兩種機制。
第二,候選人需要理解生命週期證據。pageshow 在初次載入時也會觸發,單看事件無法證明還原。pageshow 的 persisted 為 true 時,可以確認文件從 bfcache 這類快取還原。pagehide 中的 persisted: true 只表示瀏覽器打算保留頁面;頁面可能隨後被逐出,也可能再也沒有還原。
第三,優秀答案會把還原當作正確性邊界。計時器和記憶體值會從先前的時刻繼續執行。授權仍由伺服器負責,確認還原後應重新驗證敏感狀態。即時連線、資料庫控制代碼、觀察器與鎖定可能需要在離開前關閉或中斷,返回後重新開啟。重新連線邏輯必須具備冪等性。
第四,候選人應依靠證據排障,避免把一張固定阻塞清單當作答案。資格會隨瀏覽器、版本、frame、API、回應政策、記憶體壓力和導覽情境變化。Chrome DevTools 可以執行獨立 bfcache 測試並分類阻塞項。支援的環境還可透過 PerformanceNavigationTiming.notRestoredReasons 取得線上原因和 frame 樹狀結構,但原因字串可能變化,跨來源細節也可能被遮蔽。
最後,候選人應定義可衡量的結果。類型為 back_forward 的瀏覽紀錄遍歷無法單獨證明 bfcache 命中。確認命中來自 pageshow.persisted。未命中是透過瀏覽紀錄遍歷建立的新文件,應依原因分組,避免猜測。目標是提高安全還原涵蓋率,同時不出現過期工作階段操作、重複連線和失真的分析事件。
回答前要釐清的問題
- 發生了哪種導覽? 是完整文件導覽、SPA 軟式路由、重新整理、重新開啟分頁,還是同一分頁中的真實返回/前進遍歷?
- 哪些狀態可以保留? 表單草稿和捲動位置通常有價值;身分、價格、庫存、權限和一次性權杖需要權威新鮮度規則。
- 回應政策有哪些硬性要求? 某些頁面包含產品政策禁止保留在頁面快照中的資料,這項要求優先於命中率。
- 哪些資源仍處於活動狀態? 庫存 WebSocket、IndexedDB 連線、Web Locks、媒體擷取、觀察器和第三方指令碼都可能影響正確性或資格。
- 需要支援哪些瀏覽器與版本? DevTools 輸出和
notRestoredReasons都不構成可攜式契約,需用真實瀏覽紀錄導覽測試支援矩陣。 - 頁面是否包含子 frame? 同來源子 frame 可以給出詳細阻塞樹;跨來源 frame 可能只揭露遮蔽資訊,需要隔離供應商程式碼或建立最小重現。
- 「過期」如何定義? 明確最大可接受時間,以及重新驗證失敗時要禁止的操作。
- 分析口徑是什麼? 先決定一次還原算頁面瀏覽、導覽,還是兩者都算,再避免一次還原回報重複事件。
- 成功門檻是什麼? 追蹤可觀察瀏覽紀錄遍歷中的確認還原率、未命中原因、還原延遲、重新驗證失敗、過期操作攔截數和重複資源數。
30 秒回答框架
「我會先區分 bfcache、HTTP 快取和路由快取:bfcache 為瀏覽器管理的瀏覽紀錄導覽暫停整份文件。我只用 pageshow.persisted 確認命中,並用 pagehide 做冪等清理,不預設頁面一定會還原。確認還原後,我先重新驗證工作階段、購物車、價格和權限,再只重新連線一次即時資源。我會在支援的瀏覽器中重現未命中,結合 Chrome bfcache 面板、可用的 notRestoredReasons 和分瀏覽器線上指標定位原因。安全頁面盡量取得效能收益,伺服器授權和禁止快照政策始終是硬邊界。」
分步深入分析
第一步:建立四條可觀察路徑
先從結果出發,不從回應標頭出發:
| 路徑 | 是否建立新文件 | 主要證據 | 必要處理 | |---|---:|---|---| | 初次造訪或連結導覽 | 是 | pageshow.persisted === false;導覽類型通常為 navigate | 初始化狀態與資源 | | 重新整理 | 是 | pageshow.persisted === false;導覽類型為 reload | 執行一般載入路徑 | | 重新載入的瀏覽紀錄遍歷 | 是 | pageshow.persisted === false;導覽類型為 back_forward | 記錄 bfcache 未命中並檢查原因 | | 已確認的 bfcache 還原 | 否 | pageshow.persisted === true | 重新驗證敏感狀態並安全還原 |
導覽類型描述新文件如何到達,無法確認 bfcache 命中,因為命中會還原舊文件。它可以辨識由瀏覽紀錄遍歷觸發的新載入,從而進入未命中分母。瀏覽器重新啟動、複製分頁和重新開啟分頁會讓訊號變得模糊,因此線上指標應盡量攜帶瀏覽器、版本、路由和情境。
bfcache 與 HTTP 重新驗證也有清楚邊界。還原時,瀏覽器繼續執行記憶體中的頁面;像 Cache-Control: no-cache 這樣的指令不會在像素出現前觸發網路驗證。還原內容必須保持最新時,應用程式在 pageshow 後執行有針對性的驗證。SPA 軟式導覽在同一份文件中改變瀏覽紀錄和 UI;離開並返回這份文件時,bfcache 才參與其中。
第二步:讓生命週期邏輯冪等且可安全還原
集中管理連線所有權。清理之後可能出現新文件、還原舊文件,也可能永不返回。初始化會在初次載入以及多次還原後執行,所以連線和中斷都要允許重複呼叫。
let liveSocket = null;
function connectLiveUpdates() {
if (liveSocket) return;
liveSocket = new WebSocket("wss://shop.example/live-cart");
}
function disconnectLiveUpdates() {
liveSocket?.close();
liveSocket = null;
}
async function revalidateSensitiveState() {
const response = await fetch("/api/session-snapshot", {
cache: "no-store",
credentials: "same-origin",
});
if (response.status === 401) {
location.replace("/login");
return false;
}
if (!response.ok) {
showRetryState();
return false;
}
renderSessionState(await response.json());
return true;
}
window.addEventListener("pagehide", disconnectLiveUpdates);
window.addEventListener("pageshow", async (event) => {
reportNavigation(event.persisted ? "bfcache_restore" : "document_load");
if (event.persisted && !(await revalidateSensitiveState())) return;
connectLiveUpdates();
});範例中的伺服器端點仍是權威資料來源。還原後的敏感狀態完成驗證前,UI 應暫時停用結帳;驗證失敗應顯示重試狀態,不能默默信任快照。伺服器結帳請求仍需重新授權使用者,並重新計算價格和庫存。
不要用 unload 承擔關鍵清理。這個事件不可靠,也會讓某些瀏覽器中的頁面失去資格。頁面生命週期清理優先使用 pagehide;需求關注可見性時使用 visibilitychange。Chromium 專屬的 freeze 與 resume 不應成為唯一正確性路徑。
第三步:從本機重現追到未命中責任方
用確定步驟重現一個路由:直接開啟目標頁,建立相關狀態,透過一般連結進入第二份文件,再點擊瀏覽器返回。對照測試先停用擴充功能和開發工具,因為它們可能改變生命週期行為;隨後使用正式環境回應標頭和第三方指令碼重複測試。
在 Chrome 中執行 Application 面板的 Back/forward cache 測試。記錄結果屬於可操作、等待瀏覽器支援,還是不可操作,並展開 frame 歸屬。如果出現 unload 監聽器,確認註冊方來自第一方程式碼還是供應商。如果結果指向回應政策、開放連線、活動交易、鎖定、媒體 API 或 frame,就縮小到能重現問題的最小真實功能。
在支援的環境中,對由瀏覽紀錄遍歷重新建立的文件讀取 notRestoredReasons。把階層結構和原因值保留為診斷資料,不能把它們寫進產品業務分支。不要硬編碼原因字串,不要假設每個瀏覽器都支援該屬性,也不要僅憑 null 判定命中。跨來源子項可能因隱私只回傳遮蔽原因;可單獨測試嵌入供應商,或在受控實驗中移除它來建立因果關係。
每次只修一個責任方,重新執行獨立測試,再驗證完整路由。在 pagehide 關閉 IndexedDB 連線或 WebSocket、移除 unload 處理器、約束第三方 frame 都只是待驗證假設,最終以瀏覽器測試結果為準。
第四步:在線上同時驗證正確性與效能
每個 pageshow 只回報一次事件。persisted: true 表示確認還原。對於新文件載入,附帶 Navigation Timing 類型;back_forward 表示一次可觀察到的瀏覽紀錄遍歷未透過 bfcache 還原目前頁面。兩類事件應互斥。事件可以攜帶路由類別、瀏覽器、版本、身分狀態和實驗組,但不得記錄私密頁面內容。
至少衡量以下指標:
- 依瀏覽器和路由分組的確認還原數除以可觀察瀏覽紀錄遍歷數;
- 未命中數量及可用的阻塞原因類別,包括遮蔽和不支援;
- 還原到下一次繪製、還原到敏感狀態可互動的延遲;
- 工作階段、權限、購物車、價格和庫存重新驗證失敗;
- 還原狀態未確認時被攔截的結帳嘗試;
- 多次返回/前進後的重複 socket、觀察器、分析事件和計時器副作用;
- 記憶體回歸和使用者可見故障,因為命中率不是唯一目標。
測試中應重複瀏覽紀錄遍歷,在另一個分頁登出或修改購物車,讓一次性權杖過期,讓重新驗證端點失敗,並涵蓋跨來源 frame。測試範圍內的 Chrome、Firefox 與 Safari 版本都要執行。Chrome 本機面板證明一個受控案例,線上遙測證明真實分布,產品斷言證明安全性。
高品質範例答案
「瞬間還原路徑來自 bfcache:瀏覽器保存了整份結帳文件與 JavaScript 堆積,暫停它,並在工作階段瀏覽紀錄導覽中還原。HTTP 快取只保存回應,用戶端路由器處理同一份文件裡的軟式導覽。我會先標記路徑。pageshow.persisted === true 可以確認還原。Navigation Timing 類型為 back_forward 的新文件,表示瀏覽紀錄遍歷沒有讓目前頁面從 bfcache 還原。pagehide.persisted === true 只表示瀏覽器打算快取,所以我不會把它記錄成命中。」
「在 pagehide 中,我用冪等清理關閉不應保持活動的資源,例如購物車 socket 和開放交易。每次 pageshow 都只確保連線存在一次。確認還原後,我暫時停用結帳,取得權威工作階段和購物車快照,更新 UI,已登出時跳轉登入。結帳 API 仍會檢查授權、最新價格與庫存,所以還原的堆積永遠不能授權購買。分析系統只為還原計一次事件,不重播全部初次載入副作用。」
「排查未命中時,我會用正式環境回應標頭和供應商指令碼重現完整文件路徑。在 Chrome Application 面板執行 bfcache 測試,展開 frame 歸屬,每次只處理一個可操作原因。支援的 Chromium 版本可以為未命中的瀏覽紀錄載入採集 notRestoredReasons;遮蔽和未知情況會原樣保留,產品行為不依賴原因文字。隨後測試支援範圍內的 Chrome、Firefox 與 Safari,因為資格會隨實作和情境變化。」
「發布門檻是確認還原率提高、返回延遲下降,同時測試中的過期工作階段操作、重複連線和分析重複保持為零。我會依路由和瀏覽器拆分未命中及重新驗證失敗。如果產品政策規定某類私密資料不能保留在頁面快照中,我會保留限制,轉而最佳化相鄰頁面,避免以安全性換命中率。」
常見錯誤與改進
- 把所有返回路徑都叫「瀏覽器快取」 → HTTP 回應、整頁快照和路由狀態遵守不同規則 → 先命名機制,再收集對應證據。
- 只要觸發
pageshow就判定還原 → 初次文件載入也會觸發它 → 只用event.persisted === true確認還原。 - 把
pagehide.persisted當作未來命中 → 瀏覽器可能逐出頁面或選擇其他路徑 → 只用它指導安全清理,還原時再記命中。 - 認定
back_forward等於 bfcache → 它可以描述瀏覽紀錄遍歷建立的新文件 → 用導覽類型辨識未命中,用pageshow.persisted辨識命中。 - 為了修復舊 UI 而關閉全部快取 → 生命週期缺陷被掩蓋,瞬間導覽也消失 → 重新驗證敏感欄位,並保持伺服器授權權威。
- 每次還原都重新整理整頁 → 全域重新整理放棄最佳化,失敗時還可能循環 → 只更新權威狀態,把重新整理或跳轉留給明確的無效狀態。
- 背一張永久阻塞清單 → 各引擎和版本的資格與原因名稱會變化 → 使用瀏覽器測試、功能偵測和線上證據。
- 在
unload中清理 → 事件不可靠,並可能阻止資格 → 使用pagehide;可見性需求另用可見性事件。 - 在每個生命週期事件中重新連線 →
pageshow、resume和框架 effect 可能建立重複 socket 或觀察器 → 為每類資源設定一個冪等擁有者。 - 相信一次 DevTools 通過 → 它不涵蓋真實供應商、記憶體壓力、其他瀏覽器和工作階段變化 → 組合受控測試、瀏覽器矩陣、RUM 與對抗性產品流程。
追問
Cache-Control: no-cache 會在 bfcache 還原前強制驗證嗎?
不會。bfcache 還原會繼續執行記憶體中的文件,不透過 HTTP 快取滿足資源請求,所以還原頁面顯示前不會執行 HTTP 重新驗證指令。用 pageshow.persisted 觸發有針對性的新鮮度驗證。如果政策禁止保留頁面快照,應套用相應回應和瀏覽器政策,同時接受不同實作中的資格差異。
PerformanceNavigationTiming.type === "back_forward" 是命中訊號嗎?
不是。它告訴新建立的文件:目前文件透過瀏覽紀錄遍歷到達。已確認的 bfcache 還原會繼續舊文件,應透過 pageshow.persisted === true 辨識。新的 back_forward 載入可以計入可觀察未命中,但需考慮瀏覽器重新啟動、重新開啟分頁等差異,並據此分組遙測。
notRestoredReasons 缺失、為 null 或被遮蔽時怎麼辦?
缺失依不支援處理,null 本身證據不足,遮蔽則作為保護隱私的診斷類別。命中仍由頁面過渡事件確認。未命中時進行本機重現,查看可用 DevTools 輸出,隔離子 frame 和第三方指令碼,並保留「未知」分組,避免捏造原因。
分析系統怎麼處理還原頁面?
先確定產品口徑。如果還原算一次頁面瀏覽,就從 pageshow.persisted 回報一個事件,不重播初次載入埋點。加入導覽類型欄位,便於區分文件載入和還原。依需要重設本次造訪範圍的效能累計值,並測試反覆返回/前進是否重複回報。
已登入結帳頁應使用 bfcache 嗎?
安全政策允許頁面快照,且還原路徑能夠保證安全時才使用。還原後重新驗證工作階段和高影響資料,完成前阻止敏感操作,並始終執行伺服器授權與價格驗證。若私密內容不能透過瀏覽紀錄還原,就執行這項政策,並把 bfcache 最佳化放在敏感度較低的路由上。