往返快取

往返快取 (或 bfcache) 是一種瀏覽器最佳化功能,能讓使用者迅速往返網頁,大幅提升瀏覽體驗,網路/裝置速度較慢的使用者尤其有感。

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

瀏覽器相容性

所有主要瀏覽器都包含 bfcache,包括 Chrome 96 以上版本、FirefoxSafari

bfcache 基本概念

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

你造訪網站時,有多少次點按連結前往其他網頁,卻發現不是你要的網頁,然後按下「上一頁」按鈕?在這個時候,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 的主要事件是網頁轉換事件 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 如何影響 Analytics 和成效評估」。

觀察網頁進入 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 功能,這會降低風險,且會使許多網頁失去資格。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 中使用 bfcache,但之前雖然如此,但還是不可靠,因此除非絕對必要,否則請避免使用此功能。

但與 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 快取中。這類資源包含機密使用者資訊,例如需要登入才能存取的網頁。

雖然 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 還原網頁。
開發人員工具中的往返快取面板
開發人員工具中的「往返快取」面板。

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

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

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

開發人員工具回報無法從 bfcache 還原網頁
失敗的 bfcache 測試,並提供可採取的結果。

在此範例中,使用 unload 事件監聽器會導致網頁不符合 bfcache 的資格。您可以從 unload 切換為使用 pagehide 來修正這個問題:

正確做法
window.addEventListener('pagehide', ...);
錯誤做法
window.addEventListener('unload', ...);

Lighthouse 10.0 也新增了 bfcache 稽核,以執行類似的測試。詳情請參閱 bfcache 稽核說明文件

bfcache 對 Analytics 和效能評估的影響

如果您使用數據分析工具來評估網站的瀏覽量,您可能會發現,隨著 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 還原功能

其他資源