往返快取

往返快取 (或 bfcache) 是一種瀏覽器最佳化功能,能讓使用者迅速往返網頁,可大幅改善瀏覽體驗,特別是網路或裝置速度較慢的使用者。

網頁開發人員必須瞭解如何為 bfcache 最佳化網頁,讓使用者能享有這項功能的好處。

瀏覽器相容性

所有主要瀏覽器都具備 bfcache,包含 96 版以後的 Chrome、FirefoxSafari

bfcache 基本概念

啟用往返快取 (bfcache) 之後,使用者離開頁面時就不會刪除網頁,而是延後刪除並暫停執行 JS。如果使用者很快就返回,我們會再次顯示網頁,並取消暫停 JS 執行作業。這樣一來,使用者就能近乎即時地瀏覽網頁。

你造訪網站時,有多少次點按連結前往其他網頁,卻發現不是你要的網頁,然後按下「上一頁」按鈕?在這個時候,bfcache 可以大幅提升先前網頁的載入速度:

啟用 bfcache 系統會啟動新的要求來載入先前的網頁,並視該網頁針對重複造訪的最佳化程度而定,瀏覽器可能必須重新下載、重新剖析及重新執行部分 (或全部) 剛下載的資源。
啟用 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 交易過程中或使用可能影響其他網頁的 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 的主要事件是網頁轉換事件 pageshowpagehide大多數瀏覽器都支援這類事件。

當網頁進入或離開 bfcache 時,也會調度較新的網頁生命週期事件 (freezeresume),以及在其他情況下,例如為了盡量減少 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。不過,persistedtrue 並不保證網頁會快取。這表示瀏覽器「打算」快取網頁,但可能有其他因素導致無法快取。

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.');
 
}
});

同樣地,如果 persistedtruefreeze 事件會立即在 pagehide 事件後觸發,但這只代表瀏覽器「想要」快取網頁。但仍可能因其他原因而捨棄,詳情請參閱後文。

針對 bfcache 最佳化網頁

並非所有網頁都會儲存在 bfcache 中,即使網頁確實儲存在其中,也不會無限期保留在其中。開發人員必須瞭解哪些網頁符合 (或不符合) 使用 bfcache 的資格,才能盡可能提高快取命中率。

下列各節將概述最佳做法,讓瀏覽器盡可能快地快取您的網頁。

請勿使用 unload 事件

在所有瀏覽器中為 bfcache 進行最佳化時,最重要的方式就是絕對不要使用 unload 事件。永遠!

unload 事件對瀏覽器來說是個問題,因為它早於 bfcache,且許多網頁都以 (合理的) 假設運作,即網頁在 unload 事件觸發後就不會繼續存在。這會帶來挑戰,因為這些頁面假設 unload 事件會在使用者離開時觸發,但這已不再正確 (而且已很久沒有正確)。

因此瀏覽器目前面臨困境,他們必須選擇可提升使用者體驗的方法,但也可能破壞網頁。

在電腦上,Chrome 和 Firefox 選擇在網頁新增 unload 事件監聽器時,將該網頁排除在 bfcache 的使用範圍之外,雖然這麼做風險較低,但也會導致許多網頁無法使用 bfcache。Safari 會嘗試使用 unload 事件監聽器快取部分網頁,但為了減少潛在的損壞情形,系統不會在使用者離開時執行 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 事件處理常式,則可透過權限政策確保不會新增這些事件處理常式。

Permission-Policy: unload=()

這也能避免第三方或外掛程式新增卸載處理程序,導致網站無法使用 bfcache,進而導致網站速度變慢。

僅有條件地新增 beforeunload 事件監聽器

beforeunload 事件不會讓您的網頁在現代瀏覽器中無法使用 bfcache,但先前確實會,而且仍不穩定,因此除非絕對必要,否則請避免使用。

不過,beforeunloadunload 不同,有合法的用途。舉例來說,如果您想警告使用者,如果他們離開頁面,未儲存的變更就會遺失,在這種情況下,建議您只在使用者有未儲存的變更時,才新增 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 快取。這類資源包含機密使用者資訊,例如需要登入才能存取的網頁。

雖然 bfcache 不是 HTTP 快取,但在過去,當 Cache-Control: no-store 設在網頁資源本身 (而非任何子資源) 時,瀏覽器會選擇不將網頁儲存在 bfcache 中,因此任何使用 Cache-Control: no-store 的網頁可能不符合 bfcache 的使用資格。有意變更 Chrome 的這項行為,請以保護隱私權的方式運作。

由於 Cache-Control: no-store 會限制網頁使用 bfcache 的資格,因此應只在含有私密資訊的網頁上設定,因為這類網頁不應使用任何形式的快取。

如果網頁需要隨時提供最新內容,且內容不含機密資訊,請使用 Cache-Control: no-cacheCache-Control: max-age=0。這些指示會指示瀏覽器在提供內容前重新驗證內容,且不會影響網頁的 bfcache 資格。

請注意,從 bfcache 還原網頁時,系統會從記憶體還原網頁,而不是從 HTTP 快取還原。因此,系統不會將 Cache-Control: no-cacheCache-Control: max-age=0 等指令納入考量,且在內容顯示前不會重新驗證。

不過,這仍可能會帶來更好的使用者體驗,因為 bfcache 會立即還原,而且由於網頁不會在 bfcache 中停留太久,因此內容不太可能過時。不過,如果內容每分鐘都會變更,您可以使用 pageshow 事件擷取任何更新,詳情請參閱下一節。

在 bfcache 還原後更新過時或機密資料

如果您的網站會保留使用者狀態 (尤其是任何機密使用者資訊),則在從 bfcache 還原網頁後,需要更新或清除這些資料。

舉例來說,如果使用者前往結帳頁面,然後更新購物車,如果從 bfcache 還原舊版網頁,返回導覽可能會顯示過時的資訊。

另一個更嚴重的例子是,如果使用者在公用電腦上登出網站,而下一位使用者按下返回按鈕,這可能會洩漏使用者以為在登出時已清除的私人資料。

為避免發生這種情況,如果 event.persistedtrue,建議您在 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.persistedtrue 時,只在 pageshow 事件中重新整理廣告,即可在不影響網頁效能的情況下執行這項作業。請洽詢您的廣告供應商並參閱這裡的範例,瞭解如何透過 Google Publishing 代碼執行這項作業。

避免使用 window.opener 參照

在舊版瀏覽器中,如果使用 window.open() 透過含有 target=_blank 的連結開啟網頁,但未指定 rel="noopener",則開啟網頁將參照所開啟網頁的視窗物件。

除了安全性風險之外,如果網頁含有非空值 window.opener 參照,就無法安全地放入 bfcache,因為這可能會導致任何嘗試存取該網頁的網頁發生錯誤。

因此,建議您避免建立 window.opener 參照。您可以隨時使用 rel="noopener" 來達到這項目的 (請注意,這項功能目前已是所有新式瀏覽器的預設功能)。如果您的網站需要開啟視窗,並透過 window.postMessage() 或直接參照視窗物件進行控制,則開啟的視窗和開啟者都無法使用 bfcache。

在使用者離開前關閉開啟的連線

如先前所述,當網頁保留在 bfcache 中時,系統會暫停所有已排定的 JavaScript 工作,並在網頁從快取中移除時恢復這些工作。

如果這些排定的 JavaScript 工作只會存取 DOM API (或其他僅限於目前網頁的 API),那麼在使用者看不到網頁時暫停這些工作,就不會造成任何問題。

不過,如果這些工作與 API 連結,且可透過相同來源的其他網頁存取 (例如 IndexedDB、Web Locks、WebSockets),則可能會發生問題,因為暫停這些工作可能會導致其他分頁中的程式碼無法執行。

因此,在下列情況下,部分瀏覽器不會嘗試將網頁放入 bfcache:

如果網頁使用任何上述 API,強烈建議您在 pagehidefreeze 事件期間關閉連線,並移除或中斷觀察器。這樣一來,瀏覽器就能安全地快取網頁,且不會影響其他已開啟的分頁。

如果透過 bfcache 還原頁面,您可以在 pageshowresume 事件期間重新開啟或重新連線至這些 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 進行最佳化,並找出哪些問題導致網頁不符合資格。

如何測試網頁:

  1. 在 Chrome 中前往該網頁。
  2. 在 DevTools 中,依序前往「Application」->「Back-forward Cache」
  3. 按一下「Run Test」按鈕。接著,開發人員工具會嘗試離開並返回,以判斷網頁能否透過 bfcache 還原。
開發人員工具中的往返快取面板
開發人員工具中的「Back-forward Cache」面板。

如果測試成功,面板會回報「Restored from back-forward cache」。

開發人員工具回報已成功從 bfcache 還原網頁
已成功還原的頁面。

如果無法順利完成,面板會顯示原因。如果原因是開發人員可以解決的問題,面板會將其標示為「可採取行動」

開發人員工具回報失敗,無法從 bfcache 還原頁面
未通過 bfcache 測試,並提供可行的結果。

在此範例中,使用 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 快取,如前所述) 的載入速度更快。

這會導致資料集中的快速網頁載入次數減少,進而導致分布偏向較慢的情況,即使使用者體驗的成效可能有所改善也是如此!

您可以透過幾種方式處理這個問題。其中一個方法是為所有網頁載入指標加上註解,並標示各自的導覽類型navigatereloadback_forwardprerender。這樣一來,即使整體分布情形偏向負面,您也能繼續監控這些導覽類型的成效。我們建議將這種做法用於非以使用者為主的網頁載入指標,例如第一個位元組時間 (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 還原功能

其他資源