往返快取 (或 bfcache) 是一種瀏覽器最佳化功能,能讓使用者迅速往返網頁,大幅提升瀏覽體驗,網路/裝置速度較慢的使用者尤其有感。
身為網頁開發人員,瞭解如何為 bfcache 最佳化網頁至關重要,這樣使用者才能享有相關優勢。
瀏覽器相容性
所有主要瀏覽器都包含 bfcache,包括 Chrome 96 以上版本、Firefox 和 Safari。
往返快取基本概念
有了往返快取 (bfcache),使用者離開網頁時,系統不會立即銷毀網頁,而是延後銷毀並暫停執行 JS。如果使用者很快就返回,我們會再次顯示網頁,並繼續執行 JavaScript。使用者幾乎可以立即瀏覽網頁。
您是否曾造訪網站並點按連結前往其他網頁,但發現內容不符需求,因此點按返回按鈕?此時,往返快取可以大幅提升先前網頁的載入速度:
| 未啟用 bfcache | 系統會發出新要求來載入前一頁,而視該網頁的 最佳化程度,瀏覽器可能必須重新下載、剖析及執行剛下載的部分 (或全部) 資源。 |
| 啟用 bfcache | 由於整個網頁可從記憶體還原,完全不必連上網路,因此載入上一頁幾乎是瞬間完成。 |
請觀看這部影片,瞭解往返快取 (bfcache) 的運作方式,以及如何加快瀏覽速度:
影片中的範例顯示,使用 bfcache 的速度比未使用快上許多。
bfcache 不僅能加快導覽速度,還能減少資料用量,因為不必重新下載資源。
Chrome 使用資料顯示,桌機上每 10 次瀏覽中,有 1 次是返回或前進;行動裝置上每 5 次瀏覽中,有 1 次是返回或前進。啟用 bfcache 後,瀏覽器每天可省下數十億個網頁的資料傳輸量和載入時間!
「快取」的運作方式
往返快取使用的「快取」與 HTTP 快取不同,後者在加快重複瀏覽速度方面扮演自己的角色。往返快取是記憶體中整個網頁的快照,包括 JavaScript 堆積,而 HTTP 快取只包含先前發出要求的相關回應。由於從 HTTP 快取滿足載入網頁所需的所有要求非常罕見,因此使用 bfcache 還原的重複造訪速度,一律比經過最佳化的非 bfcache 瀏覽更快。
凍結頁面以便日後重新啟用,在如何妥善保留進行中的程式碼方面,涉及一些複雜性。舉例來說,如果網頁位於 bfcache 中,且已達到逾時時間,您會如何處理 setTimeout() 呼叫?
答案是,瀏覽器會暫停 bfcache 中網頁的所有待處理計時器或未解決的 Promise,包括 JavaScript 工作佇列中的幾乎所有待處理工作,並在網頁從 bfcache 還原時繼續處理工作。
在某些情況下 (例如逾時和 Promise),這種做法的風險相當低,但在其他情況下,可能會導致混淆或非預期的行為。舉例來說,如果瀏覽器暫停某項工作,而該工作是 IndexedDB 交易的一部分,則可能會影響相同來源的其他開啟分頁,因為多個分頁可以同時存取相同的 IndexedDB 資料庫。因此,瀏覽器通常不會嘗試在 IndexedDB 交易期間或使用可能影響其他網頁的 API 時,快取網頁。
如要進一步瞭解各種 API 用法對網頁 bfcache 資格的影響,請參閱「針對 bfcache 最佳化網頁」。
bfcache 和 iframe
如果網頁含有嵌入的 iframe,iframe 本身不符合 bfcache 的資格。舉例來說,如果您在 iframe 中前往其他網址,先前的內容不會進入 bfcache,而如果您返回,瀏覽器會在 iframe 中「返回」,而不是在主框架中,但 iframe 中的返回導覽不會使用 bfcache。
不過,當主要框架從 bfcache 還原時,內嵌 iframe 會還原為網頁進入 bfcache 時的狀態。
如果內嵌 iframe 使用會封鎖往返快取的 API,主框架也可能無法使用往返快取。如要避免發生這種情況,請在主要框架中設定權限政策,或使用 sandbox 屬性。
bfcache 和單頁應用程式 (SPA)
由於 bfcache 適用於瀏覽器管理的瀏覽動作,因此不適用於單頁應用程式 (SPA) 內的「軟性瀏覽」。不過,當您返回 SPA 時,bfcache 仍可派上用場,不必從頭重新完整初始化該應用程式。
用於觀察往返快取的 API
雖然 bfcache 是瀏覽器自動執行的最佳化作業,但開發人員仍須瞭解何時會發生這種情況,才能針對 bfcache 最佳化網頁,並據此調整任何指標或成效評估。
觀察 bfcache 時主要使用的事件是網頁轉換事件 pageshow 和 pagehide,大多數瀏覽器都支援這些事件。
當網頁進入或離開 bfcache,以及在其他情況下 (例如背景分頁遭凍結以減少 CPU 使用量),系統也會傳送較新的「網頁生命週期」事件 (freeze 和 resume)。這些事件僅適用於以 Chromium 為基礎的瀏覽器。
觀察網頁從 bfcache 還原時的情況
網頁初次載入時,系統會在 load 事件後立即觸發 pageshow 事件,且每次從往返快取還原網頁時也會觸發。pageshow 事件具有 persisted 屬性,如果網頁是從 bfcache 還原,則為 true,否則為 false。您可以使用 persisted 屬性,區分一般網頁載入和 bfcache 還原。例如:
window.addEventListener('pageshow', (event) => {
if (event.persisted) {
console.log('This page was restored from the bfcache.');
} else {
console.log('This page was loaded normally.');
}
});
在支援 Page Lifecycle API 的瀏覽器中,當網頁從 bfcache 還原時 (緊接在 pageshow 事件之前),以及使用者重新造訪凍結的背景分頁時,就會觸發 resume 事件。如要在網頁凍結後更新網頁狀態 (包括 bfcache 中的網頁),可以使用 resume 事件,但如要評估網站的 bfcache 命中率,則需要使用 pageshow 事件。在某些情況下,您可能需要同時使用這兩者。
如要瞭解 bfcache 評估最佳做法,請參閱「bfcache 如何影響 Analytics 和成效評估」。
觀察網頁何時進入 bfcache
pagehide 事件會在網頁卸載時或瀏覽器嘗試將網頁放入 bfcache 時觸發。
pagehide 事件也具有 persisted 屬性。如果是 false,您就能確信該網頁不會進入 bfcache。不過,persisted即使是 true,也不保證網頁會快取。這表示瀏覽器打算快取網頁,但可能因為其他因素而無法快取。
window.addEventListener('pagehide', (event) => {
if (event.persisted) {
console.log('This page *might* be entering the bfcache.');
} else {
console.log('This page will unload normally and be discarded.');
}
});
同樣地,如果 persisted 為 true,freeze 事件會在 pagehide 事件後立即觸發,但這只表示瀏覽器打算快取網頁。但基於稍後說明的多項原因,系統可能仍須捨棄該要求。
針對 bfcache 最佳化網頁
並非所有網頁都會儲存在往返快取中,即使網頁儲存在往返快取中,也不會無限期保留。開發人員務必瞭解網頁是否符合 bfcache 資格,才能盡量提高快取命中率。
以下各節將說明最佳做法,盡可能讓瀏覽器快取網頁。
請勿使用 unload 事件
在所有瀏覽器中,最佳化 bfcache 最重要的方法就是絕不使用 unload 事件。Ever!
對瀏覽器來說,unload 事件會造成問題,因為這個事件早於 bfcache,且網路上許多網頁都 (合理地) 假設 unload 事件觸發後,網頁就不會繼續存在。這是一項挑戰,因為許多網頁也是根據使用者離開時會觸發 unload 事件的假設所建構,但這已不再成立 (長期以來都不成立)。
因此瀏覽器面臨兩難,必須在改善使用者體驗和網頁可能無法正常運作之間做出選擇。
在桌機上,Chrome 和 Firefox 選擇在網頁新增 unload 監聽器時,讓網頁不符合 bfcache 資格,這樣做風險較低,但也會導致許多網頁不符合資格。Safari 會嘗試快取部分含有 unload 事件監聽器的網頁,但為了減少潛在的損壞,使用者離開時不會執行 unload 事件,因此這個事件非常不可靠。
在行動裝置上,Chrome 和 Safari 會嘗試快取含有 unload 事件監聽器的網頁,因為 unload 事件在行動裝置上一直極不可靠,因此發生中斷的風險較低。Firefox 會將使用 unload 的網頁視為不符合往返快取資格,但 iOS 除外。iOS 規定所有瀏覽器都必須使用 WebKit 算繪引擎,因此行為與 Safari 類似。
請改用 pagehide 事件,而非 unload 事件。在所有會觸發 unload 事件的情況下,系統都會觸發 pagehide 事件,且網頁進入 bfcache 時也會觸發這個事件。
事實上,Lighthouse 具有 no-unload-listeners 稽核功能,如果網頁上的任何 JavaScript (包括來自第三方程式庫的 JavaScript) 新增 unload 事件監聽器,就會向開發人員發出警告。
由於 unload 事件不可靠,且會影響 bfcache 的效能,因此 Chrome 打算淘汰這項事件。
使用權限政策禁止在網頁上使用卸載處理常式
如果網站未使用 unload 事件處理常式,可以透過權限政策確保系統不會新增這些常式。
Permissions-Policy: unload=()
這也能防止第三方或擴充功能新增卸載處理常式,導致網站速度變慢,並使網站不符合 bfcache 的資格。
有條件地新增 beforeunload 監聽器
在現代瀏覽器的往返快取中,beforeunload 事件不會導致網頁不符合往返快取資格,但先前會,而且目前仍不可靠,因此除非絕對必要,否則請避免使用。
不過,與 unload 事件不同的是,beforeunload 有正當用途。舉例來說,當使用者有未儲存的變更,如果離開頁面就會遺失,您可能想警告使用者。在這種情況下,建議您只在使用者有未儲存的變更時新增 beforeunload 監聽器,並在儲存未儲存的變更後立即移除。
window.addEventListener('beforeunload', (event) => { if (pageHasUnsavedChanges()) { event.preventDefault(); return event.returnValue = 'Are you sure you want to exit?'; } });
beforeunload 監聽器。
function beforeUnloadListener(event) { event.preventDefault(); return event.returnValue = 'Are you sure you want to exit?'; }; // A function that invokes a callback when the page has unsaved changes. onPageHasUnsavedChanges(() => { window.addEventListener('beforeunload', beforeUnloadListener); }); // A function that invokes a callback when the page's unsaved changes are resolved. onAllChangesSaved(() => { window.removeEventListener('beforeunload', beforeUnloadListener); });
beforeunload 監聽器,並在不需要時移除。
盡可能避免使用 Cache-Control: no-store
Cache-Control: no-store 是網路伺服器可在回應中設定的 HTTP 標頭,可指示瀏覽器不要將回應儲存在任何 HTTP 快取中。這項功能適用於含有機密使用者資訊的資源,例如需要登入才能存取的頁面。
雖然往返快取不是 HTTP 快取,但從歷史來看,如果是在網頁資源本身 (而非任何子資源) 上設定 Cache-Control: no-store,瀏覽器會選擇不將網頁儲存在往返快取中,因此使用 Cache-Control: no-store 的網頁可能不適用往返快取。我們正努力以兼顧隱私的方式,變更 Chrome 的這項行為。
由於 Cache-Control: no-store 會限制網頁是否符合 bfcache 資格,因此只有在含有私密資訊的網頁上才應設定這項屬性,因為這類網頁不適合進行任何形式的快取。
如要確保網頁一律提供最新內容,且內容不含私密資訊,請使用 Cache-Control: no-cache 或 Cache-Control: max-age=0。這些指令會指示瀏覽器在提供內容前重新驗證,且不會影響網頁的 bfcache 資格。
請注意,從 bfcache 還原網頁時,是從記憶體還原,而非從 HTTP 快取還原。因此,系統不會將 Cache-Control: no-cache 或 Cache-Control: max-age=0 等指令納入考量,也不會在向使用者顯示內容前重新驗證。
不過,由於 bfcache 還原是即時的,且網頁不會在 bfcache 中停留太久,因此內容不太可能過時,這仍可能帶來更優質的使用者體驗。不過,如果內容每分鐘都會變更,您可以按照下一節所述,使用 pageshow 事件擷取任何更新。
在 bfcache 還原後更新過時或機密資料
如果網站會保留使用者狀態 (尤其是任何私密使用者資訊),則網頁從 bfcache 還原後,必須更新或清除該資料。
舉例來說,如果使用者前往結帳頁面,然後更新購物車,系統從 bfcache 還原過時的頁面時,返回導覽可能會顯示過時的資訊。
另一個更重要的例子是,假設使用者在公用電腦上登出網站,而下一個使用者點選返回按鈕,這可能會導致使用者登出時以為已清除的私人資料外洩。
為避免發生這種情況,建議您在 event.persisted 為 true 時,一律在 pageshow 事件後更新網頁:
window.addEventListener('pageshow', (event) => {
if (event.persisted) {
// Do any checks and updates to the page
}
});
雖然最好是就地更新內容,但對於某些變更,您可能需要強制完整重新載入。下列程式碼會檢查 pageshow 事件中是否有網站專屬 Cookie,如果沒有,就會重新載入:
window.addEventListener('pageshow', (event) => {
if (event.persisted && !document.cookie.match(/my-cookie)) {
// Force a reload if the user has logged out.
location.reload();
}
});
重新載入的優點是仍會保留記錄 (允許向前導覽),但在某些情況下,重新導向可能更合適。
廣告和 bfcache 還原
您可能會想避免使用往返快取,以便在每次返回/前往瀏覽時放送一組新廣告。不過,這類行為不僅會影響成效,是否能提升廣告參與度也令人存疑。使用者可能看到想點按的廣告,但重新載入頁面後,而非從 bfcache 還原,因此無法點按。請務必先測試這個情境 (最好是進行 A/B 測試),再做出假設。
如果網站希望在 bfcache 還原時重新整理廣告,則在 pageshow 事件發生時,僅重新整理 event.persisted 上的廣告,即可在不影響網頁效能的情況下達成此目的。true請洽詢您的廣告供應商,但這裡提供一個範例,說明如何使用 Google 發布商廣告代碼執行這項操作。
避免使用 window.opener 參考資料
在舊版瀏覽器中,如果網頁是透過 window.open() 從含有 target=_blank 的連結開啟,且未指定 rel="noopener",開啟的網頁就會參照開啟網頁的視窗物件。
除了有安全性風險,具有非空值 window.opener 參照的網頁也無法安全地放入 bfcache,因為這可能會導致嘗試存取該參照的網頁發生錯誤。
因此,最好避免建立 window.opener 參照。盡可能使用 rel="noopener" 即可 (請注意,這現在是所有新式瀏覽器的預設值)。如果網站需要開啟視窗並透過 window.postMessage() 控制視窗,或直接參照視窗物件,開啟的視窗和開啟者都不符合 bfcache 的資格。
在使用者離開前關閉開啟的連線
如先前所述,網頁保留在往返快取中時,系統會暫停所有排定的 JavaScript 工作,並在網頁從快取中取出時繼續執行。
如果這些排定的 JavaScript 工作只會存取 DOM API,或是僅限於目前網頁的其他 API,那麼在使用者看不到網頁時暫停這些工作,就不會造成任何問題。
不過,如果這些工作連結至可從相同來源的其他網頁存取的 API (例如 IndexedDB、Web Locks、WebSockets),這可能會造成問題,因為暫停這些工作可能會導致其他分頁中的程式碼無法執行。
因此,在下列情況下,部分瀏覽器不會嘗試將網頁放入 bfcache:
- 含有開放式 IndexedDB 連線的網頁
- 使用 fetch() 或 XMLHttpRequest 進行中的網頁
- 開啟 WebSocket 或 WebRTC 連線的網頁
如果網頁使用上述任一 API,強烈建議您在 pagehide 或 freeze 事件期間關閉連線,並移除或中斷觀察器。這樣一來,瀏覽器就能安全地快取網頁,不會影響其他開啟的分頁。
接著,如果網頁是從 bfcache 還原,您可以在 pageshow 或 resume 事件期間重新開啟或重新連線至這些 API。
以下範例說明如何關閉 pagehide 事件監聽程式中的開啟連線,確保使用 IndexedDB 的網頁符合 bfcache 資格:
let dbPromise;
function openDB() {
if (!dbPromise) {
dbPromise = new Promise((resolve, reject) => {
const req = indexedDB.open('my-db', 1);
req.onupgradeneeded = () => req.result.createObjectStore('keyval');
req.onerror = () => reject(req.error);
req.onsuccess = () => resolve(req.result);
});
}
return dbPromise;
}
// Close the connection to the database when the user leaves.
window.addEventListener('pagehide', () => {
if (dbPromise) {
dbPromise.then(db => db.close());
dbPromise = null;
}
});
// Open the connection when the page is loaded or restored from bfcache.
window.addEventListener('pageshow', () => openDB());
測試網頁是否可快取
Chrome 開發人員工具可協助您測試網頁,確保網頁已針對 bfcache 進行最佳化,並找出可能導致網頁不符合資格的問題。
如要測試網頁,請按照下列步驟操作:
- 在 Chrome 中前往該頁面。
- 在開發人員工具中,依序前往「應用程式」->「返回/快取」。
- 按一下「執行測試」按鈕。開發人員工具接著會嘗試離開並返回,判斷網頁是否可從 bfcache 還原。
如果測試成功,面板會顯示「從往返快取還原」。
如果無法順利連結,面板會顯示原因。如果開發人員可以解決問題,面板會將原因標示為「可採取行動」。
在本範例中,使用 unload 事件監聽器會導致網頁不符合往返快取資格。如要修正這個問題,請從 unload 切換為使用 pagehide:
window.addEventListener('pagehide', ...);
window.addEventListener('unload', ...);
Lighthouse 10.0 也新增了 bfcache 稽核,可執行類似的測試。詳情請參閱 bfcache 稽核說明文件。
bfcache 對 Analytics 和成效評估的影響
如果您使用分析工具評估網站造訪次數,可能會發現 Chrome 為更多使用者啟用 bfcache 後,系統回報的網頁瀏覽總次數減少。
事實上,您可能已經低估了其他實作 bfcache 的瀏覽器網頁瀏覽量,因為許多熱門的 Analytics 程式庫不會將 bfcache 還原視為新的網頁瀏覽。
如要在網頁瀏覽計數中納入 bfcache 還原作業,請為 pageshow 事件設定監聽器,並檢查 persisted 屬性。
以下範例說明如何使用 Google Analytics 執行這項操作。其他分析工具可能也會採用類似的邏輯:
// Send a pageview when the page is first loaded.
gtag('event', 'page_view');
window.addEventListener('pageshow', (event) => {
// Send another pageview if the page is restored from bfcache.
if (event.persisted) {
gtag('event', 'page_view');
}
});
評估 bfcache 命中率
您也可以評估是否使用了往返快取,找出未採用往返快取的網頁。方法是測量網頁載入的導覽類型:
// Send a navigation_type when the page is first loaded.
gtag('event', 'page_view', {
'navigation_type': performance.getEntriesByType('navigation')[0].type;
});
window.addEventListener('pageshow', (event) => {
if (event.persisted) {
// Send another pageview if the page is restored from bfcache.
gtag('event', 'page_view', {
'navigation_type': 'back_forward_cache';
});
}
});
使用 back_forward 導覽和 back_forward_cache 導覽的計數,計算 bfcache 命中率。
請務必瞭解,在許多情況下,返回/前進導覽不會使用 bfcache,且這些情況並非網站擁有者可控,包括:
- 使用者關閉瀏覽器並重新啟動時
- 使用者複製分頁時
- 使用者關閉分頁並重新開啟時
在某些情況下,部分瀏覽器可能會保留原始的導覽類型,因此即使不是「返回」/「前往」導覽,也可能會顯示 back_forward 類型。
即使沒有這些排除項目,系統也會在一段時間後捨棄 bfcache,以節省記憶體。
因此,網站擁有者不應預期所有 back_forward 導覽的 bfcache 命中率都會達到 100%。不過,測量這類比例有助於找出網頁,因為在這些網頁中,網頁本身會阻止高比例的返回和前進瀏覽使用 bfcache。
Chrome 團隊已新增 NotRestoredReasons API,可協助揭露網頁未使用 bfcache 的原因,讓開發人員提高 bfcache 命中率。Chrome 團隊也在 CrUX 中新增了導覽類型,即使您未自行測量,也能查看 bfcache 導覽次數。
成效評估
bfcache 也可能對現場收集的成效指標造成負面影響,尤其是用來評估網頁載入時間的指標。
由於往返快取瀏覽會還原現有網頁,而不是啟動新的網頁載入作業,因此啟用往返快取後,系統收集到的網頁載入總數會減少。不過,最重要的是,由 bfcache 還原取代的網頁載入,可能是資料集中載入速度最快的網頁。這是因為往返瀏覽的定義就是重複造訪,而重複載入網頁通常比首次訪客載入網頁的速度更快 (如先前所述,這是因為 HTTP 快取)。
因此資料集中快速載入的網頁會減少,即使使用者體驗的效能可能有所提升,但分配速度仍可能變慢!
有幾種方法可以解決這個問題,方法之一是使用各自的導覽類型 (navigate、reload、back_forward 或 prerender) 註解所有網頁載入指標。即使整體分配比例偏向負面,您仍可繼續監控這些導覽類型的成效。如果是「Time to First Byte (TTFB)」等非以使用者為中心的網頁載入指標,建議採用這種做法。
對於以使用者為中心的指標 (例如網站使用體驗核心指標),較好的做法是回報能更準確反映使用者體驗的值。
對網站體驗核心指標的影響
網站使用體驗核心指標會從各種層面 (載入速度、互動性、視覺穩定性) 評估網頁的使用者體驗。由於使用者會將 bfcache 還原視為比完整網頁載入更快速的導覽,因此網站使用體驗核心指標必須反映這一點。畢竟使用者不在乎是否啟用 bfcache,只在乎導覽速度是否夠快!
收集及回報 Core Web Vitals 指標的工具 (例如 Chrome 使用者體驗報告),會將 bfcache 還原視為資料集中的個別網頁造訪。雖然沒有專用的網頁效能 API 可在 bfcache 還原後評估這些指標,但您可以使用現有的網頁 API 估算這些值:
- 如果是最大內容繪製 (LCP),請使用
pageshow事件的時間戳記與下一個繪製影格的時間戳記之間的差異,因為影格中的所有元素都會同時繪製。如果是 bfcache 還原,LCP 和 FCP 相同。 - 如要取得 Interaction to Next Paint (INP),請繼續使用現有的 Performance Observer,但將目前的 INP 值重設為 0。
- 如要取得累計版面配置位移 (CLS),請繼續使用現有的 Performance Observer,但將目前的 CLS 值重設為 0。
如要進一步瞭解 bfcache 對各項指標的影響,請參閱個別 Core Web Vitals 指標指南頁面。如需如何實作這些指標 bfcache 版本的具體範例,請參閱將這些指標新增至 web-vitals JS 程式庫的 PR。
web-vitals JavaScript 程式庫支援在回報指標時還原 bfcache。
其他資源
- Firefox 快取 (Firefox 中的 bfcache)
- 網頁快取 (Safari 中的 bfcache)
- 往返快取:網頁公開行為 (各瀏覽器的 bfcache 差異)
- bfcache 測試工具 (測試不同 API 和事件對瀏覽器 bfcache 的影響)
- 效能大躍進:瀏覽器往返快取 (Smashing Magazine 的案例研究,顯示啟用往返快取後,核心網頁指標大幅改善)