往返快取 (或 bfcache) 是一種瀏覽器最佳化功能,能讓使用者迅速往返網頁,大幅提升瀏覽體驗,網路/裝置速度較慢的使用者尤其有感。
身為網頁開發人員,您必須瞭解如何為 bfcache 最佳化網頁,讓使用者能享有這項功能的好處。
瀏覽器相容性
所有主要瀏覽器都包含 bfcache,包括 Chrome 96 以上版本、Firefox 和 Safari。
bfcache 基本概念
使用往返快取 (bfcache) 時,我們會延後銷毀作業並暫停 JS 執行作業,而不是在使用者離開時銷毀網頁。如果使用者很快就返回,我們會再次顯示網頁,並取消暫停 JS 執行作業。這樣一來,使用者就能幾乎立即瀏覽網頁。
你造訪網站時,有多少次點按連結前往其他網頁,卻發現不是你要的網頁,然後按下「上一頁」按鈕?在這個時候,bfcache 可以大幅提升先前網頁的載入速度:
未啟用 bfcache | 系統會啟動新的要求來載入先前的網頁,並視該網頁針對重複造訪的 最佳化程度而定,瀏覽器可能必須重新下載、重新剖析及重新執行部分 (或全部) 剛下載的資源。 |
啟用 bfcache | 載入先前網頁「基本上」是即時的,因為整個網頁可以從記憶體還原,完全不需要連線。 |
請觀看這部 bfcache 運作影片,瞭解這項功能可加快導覽速度:
在影片中,使用 bfcache 的範例比不使用 bfcache 的範例快上許多。
由於資源不必再次下載,因此 bfcache 不僅可加快導覽速度,還能減少資料用量。
Chrome 使用資料顯示,在電腦上有 10 次導覽中有 1 次是前進/後退,在行動裝置上則有 5 次中有 1 次是前進/後退。啟用 bfcache 後,瀏覽器就能省去每天要花費的資料傳輸時間,以及為數十億個網頁載入資料的時間!
「快取」的運作方式
bfcache 使用的「快取」與 HTTP 快取不同,後者可用於加快重複瀏覽的速度。bfcache 是記憶體中整個網頁的快照,包括 JavaScript 堆積,而 HTTP 快取只包含先前要求的回應。由於載入網頁所需的所有要求很少會從 HTTP 快取中滿足,因此使用 bfcache 還原功能的多次造訪速度一律會比使用經過最佳化處理的非 bfcache 導覽更快。
凍結網頁以便日後重新啟用時,在如何保留進行中的程式碼方面會涉及一些複雜性。舉例來說,如果網頁位於 bfcache 中,且 setTimeout()
呼叫已達到逾時期限,您該如何處理?
答案是瀏覽器會暫停 bfcache 中任何待處理的計時器或未解決的承諾,包括 JavaScript 工作佇列中幾乎所有待處理的工作,並在頁面從 bfcache 還原時恢復處理工作。
在某些情況下,例如在逾時和承諾方面,這項做法風險相當低,但在其他情況下,可能會導致令人困惑或意外的行為。舉例來說,如果瀏覽器暫停 IndexedDB 交易所需的任務,可能會影響同一個來源中其他已開啟的分頁,因為同一個 IndexedDB 資料庫可同時由多個分頁存取。因此,瀏覽器通常不會在 IndexedDB 交易過程中或使用可能影響其他網頁的 API 時,嘗試快取網頁。
如要進一步瞭解各種 API 用途對網頁 bfcache 資格的影響,請參閱「為 bfcache 最佳化網頁」。
bfcache 和 iframe
如果網頁含有嵌入的 iframe,則 iframe 本身不適用於 bfcache。舉例來說,如果您在 iframe 中前往其他頁面,然後返回,瀏覽器會在 iframe 中返回,而不是在主頁框中返回,但 iframe 中的返回導覽不會使用 bfcache。
如果嵌入式 iframe 使用會封鎖此功能的 API,主框架也可能會遭到封鎖,無法使用 bfcache。您可以使用主要框架上的權限政策或 sandbox
屬性來避免這種情況。
bfcache 和單頁應用程式 (SPA)
由於 bfcache 可搭配瀏覽器管理的導覽功能,因此不適用於單頁應用程式 (SPA) 中的「軟性導覽」功能。不過,如果您要返回 SPA,而不是從頭開始重新初始化該應用程式,bfcache 仍可提供協助。
用於觀察 bfcache 的 API
雖然 bfcache 是瀏覽器自動執行的最佳化功能,但開發人員仍應瞭解何時會發生這項最佳化,以便針對 bfcache 最佳化網頁,並調整任何指標或成效評估。
用於觀察 bfcache 的主要事件是網頁轉換事件 pageshow
和 pagehide
,大部分瀏覽器都支援這類事件。
當網頁進入或離開 bfcache 時,也會調度較新的網頁生命週期事件 (freeze
和 resume
),以及在其他情況下,例如為了盡量減少 CPU 使用量而凍結背景分頁時。這些事件僅適用於以 Chromium 為基礎的瀏覽器。
觀察網頁何時從 bfcache 還原
當網頁初次載入,以及從 bfcache 還原網頁時,pageshow
事件會在 load
事件後立即觸發。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 如何影響數據分析和成效評估」。
觀察網頁何時進入 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 中,即使網頁儲存在 bfcache 中,也不會無限期保留在其中。開發人員必須瞭解哪些網頁符合 (或不符合) 使用 bfcache 的資格,才能盡可能提高快取命中率。
下列各節將概述最佳做法,讓瀏覽器盡可能快地快取您的網頁。
請勿使用 unload
事件
在所有瀏覽器中為 bfcache 進行最佳化時,最重要的方式就是絕對不要使用 unload
事件。永遠!
unload
事件對瀏覽器來說是個問題,因為它早於 bfcache,且許多網頁都以合理的假設運作,即在 unload
事件觸發後,網頁不會繼續存在。這會帶來挑戰,因為這些頁面也假設 unload
事件會在使用者離開時觸發,但這已不再正確 (而且很久以前就不再正確)。
因此,瀏覽器面臨兩難的處境,必須在能改善使用者體驗,但也可能導致網頁無法正常運作之間做出選擇。
在電腦上,Chrome 和 Firefox 選擇在網頁新增 unload
事件監聽器時,將該網頁排除在 bfcache 的使用範圍之外,雖然這麼做風險較低,但也會導致許多網頁無法使用 bfcache。Safari 會嘗試使用 unload
事件監聽器快取部分網頁,但為了減少潛在的損壞情形,Safari 不會在使用者離開時執行 unload
事件,這會讓事件變得非常不可靠。
在行動裝置上,Chrome 和 Safari 會嘗試使用 unload
事件事件監聽器快取網頁,因為 unload
事件在行動裝置上一直非常不可靠,因此發生錯誤的風險較低。Firefox 會將使用 unload
的網頁視為不符合使用 bfcache 的資格,但 iOS 除外,因為 iOS 要求所有瀏覽器都使用 WebKit 轉譯引擎,因此會像 Safari 一樣運作。
請改用 pagehide
事件,而非 unload
事件。pagehide
事件會在 unload
事件觸發的所有情況下觸發,且也會在網頁放入 bfcache 時觸發。
事實上,Lighthouse 提供 no-unload-listeners
稽核,如果網頁上的任何 JavaScript (包括第三方程式庫中的 JavaScript) 新增 unload
事件監聽器,就會向開發人員發出警告。
由於 unload
事件不穩定且會影響 bfcache 效能,Chrome 打算淘汰這項事件。
使用權限政策,避免在頁面上使用卸載處理常式
如果網站未使用 unload
事件處理常式,則可透過權限政策確保不會新增這些事件處理常式。
Permissions-Policy: unload=()
這也能避免第三方或擴充功能新增卸載處理常式,導致網站無法使用 bfcache,進而導致網站速度變慢。
僅在特定條件下新增 beforeunload
監聽器
beforeunload
事件不會讓您的網頁在現代瀏覽器中無法使用 bfcache,但先前確實會,而且仍不穩定,因此除非絕對必要,否則請避免使用。
不過,beforeunload
與 unload
不同,有合法的用途。舉例來說,如果您想警告使用者,如果離開頁面,他們會失去未儲存的變更,在這種情況下,建議您只在使用者有未儲存的變更時,才新增 beforeunload
事件監聽器,然後在未儲存的變更儲存後立即移除。
window.addEventListener('beforeunload', (event) => { if (pageHasUnsavedChanges()) { event.preventDefault(); return event.returnValue = 'Are you sure you want to exit?'; } });
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); });
盡量減少使用 Cache-Control: no-store
Cache-Control: no-store
是網路伺服器可在回應中設定的 HTTP 標頭,可指示瀏覽器不要將回應儲存在任何 HTTP 快取中。這類資源包含機密使用者資訊,例如需要登入才能瀏覽的網頁。
雖然 bfcache 不是 HTTP 快取,但在過去,當 Cache-Control: no-store
設在網頁資源本身 (而非任何子資源) 時,瀏覽器會選擇不將網頁儲存在 bfcache 中,因此任何使用 Cache-Control: no-store
的網頁可能不符合 bfcache 的使用資格。我們正在著手改變 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,如果找不到 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,以便在每次返回/前進瀏覽時放送新廣告。不過,除了影響成效之外,這類行為是否能提升廣告參與度仍有待商榷。使用者可能會發現,他們原本想點按的廣告,在重新載入時無法從 bfcache 還原,在做出假設前,請務必測試這個情境,最好是採用 A/B 版本測試。
如果網站確實想在 bfcache 還原時重新整理廣告,只要在 event.persisted
為 true
時,只在 pageshow
事件中重新整理廣告,即可在不影響網頁效能的情況下執行這項作業。請洽詢廣告供應商,但這裡有一個範例,說明如何使用 Google 發布商廣告代碼執行這項操作。
避免使用 window.opener
參照
在舊版瀏覽器中,如果使用含有 target=_blank
的連結,透過 window.open()
開啟網頁,而未指定 rel="noopener"
,開啟網頁就會參照已開啟網頁的視窗物件。
除了安全性風險之外,如果網頁含有非空值 window.opener
參照,就無法安全地放入 bfcache,因為這可能會導致任何嘗試存取該網頁的網頁發生錯誤。
因此,建議您避免建立 window.opener
參照。您可以隨時使用 rel="noopener"
來執行此操作 (請注意,這項功能目前已是所有新式瀏覽器的預設功能)。如果您的網站需要開啟視窗,並透過 window.postMessage()
或直接參照視窗物件進行控制,則開啟的視窗和開啟者都無法使用 bfcache。
在使用者離開前關閉已開啟的連線
如先前所述,當網頁保留在 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 中前往該網頁。
- 在開發人員工具中,依序前往「Application」->「Back-forward Cache」。
- 按一下「Run Test」按鈕。開發人員工具接著會嘗試離開及返回,以判斷是否可以從 bfcache 還原網頁。
如果測試成功,面板會回報「Restored from back-forward cache」。
如果無法順利完成,面板會顯示原因。如果原因是開發人員可以解決的問題,面板會將其標示為「可採取行動」。
在這個範例中,使用 unload
事件監聽器會使網頁不符合使用 bfcache。您可以從 unload
切換為使用 pagehide
來修正這個問題:
window.addEventListener('pagehide', ...);
window.addEventListener('unload', ...);
Lighthouse 10.0 也新增了 bfcache 稽核,可執行類似的測試。詳情請參閱 bfcache 稽核說明文件。
bfcache 對數據分析和效能評估的影響
如果您使用數據分析工具來評估網站的瀏覽量,您可能會發現,隨著 Chrome 為更多使用者啟用 bfcache,網頁瀏覽量的總數會逐漸減少。
事實上,您可能已經低估了實作 bfcache 的其他瀏覽器的網頁瀏覽次數,因為許多熱門的數據分析程式庫並未將 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 命中率
您也可以評估是否使用了 bfcache,以便找出未使用 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 導覽會還原現有網頁,而非啟動新的網頁載入作業,因此啟用 bfcache 後,系統收集的網頁載入總數會減少。不過,重要的是,由 bfcache 還原作業取代的網頁載入作業,很可能是資料集中載入速度最快的網頁。這是因為往返瀏覽的定義是重複造訪,而重複網頁載入通常比初次造訪者的網頁載入速度更快 (這是因為前面提到的 HTTP 快取)。
這會導致資料集中的快速網頁載入次數減少,進而使分布偏向較慢的載入時間,即使使用者體驗到的效能可能有所改善!
您可以透過幾種方式處理這個問題。其中一個方法是為所有網頁載入指標加上註解,並標示各自的導覽類型:navigate
、reload
、back_forward
或 prerender
。這樣一來,即使整體分布情形偏向負面,您也能繼續監控這些導覽類型的成效。我們建議將這種做法用於非以使用者為主的網頁載入指標,例如第一個位元組時間 (TTFB)。
對於以使用者為中心的指標 (例如 Core Web Vitals),建議您回報的值更準確地反映使用者體驗。
對 Core Web Vitals 的影響
Core Web Vitals 會從多個面向 (載入速度、互動性、視覺穩定性) 評估使用者對網頁的體驗。由於使用者會認為 bfcache 還原功能比起整個網頁載入功能更快,因此 Core Web Vitals 指標必須反映這項體驗。畢竟使用者不關心是否啟用 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 的案例研究顯示,啟用 bfcache 後,核心網頁生命週期指標大幅改善)