往返快取

往返快取 (或 bfcache) 是一種瀏覽器最佳化功能,可以 瀏覽上一個或下一個頁面這項功能可大幅改善瀏覽體驗 尤其是網路或裝置速度較慢的使用者

身為網頁程式開發人員,瞭解如何最佳化網頁以採用 bfcache 快取,讓使用者能夠享受同樣的好處。

瀏覽器相容性

多年來,FirefoxSafari 已支援 bfcache 的電腦和行動裝置。

自版本 86 起,Chrome 已為少數使用者啟用 bfcache,在 Android 上進行跨網站瀏覽。在後續版本中,我們會逐步推出額外支援。自 96 版起,我們已對電腦和行動裝置上的所有 Chrome 使用者啟用 bfcache。

bfcache 基本概念

bfcache 是記憶體內快取,可在使用者離開網頁時儲存網頁 (包括 JavaScript 堆積) 的完整快照。至於記憶體中的整個頁面,瀏覽器可以在使用者決定返回時快速還原。

您有多少次造訪某個網站,並且點擊連結前往其他網頁,但結果才發現這不是您想要的結果,並點選返回按鈕?此時,bfcache 可能會大幅改變上一頁的載入速度:

不啟用 bfcache 系統會發出新的要求來載入上一頁;此外,視網頁針對重複造訪的 最佳化程度而定,瀏覽器可能需要重新下載、重新剖析並重新執行部分 (或全部) 剛下載的資源。
啟用 bfcache 載入上一頁後,基本上能夠從記憶體還原整個網頁,不必到網路就能還原。

請觀看以下影片 bfcache 實際操作影片,瞭解 bfcache 如何提升導航速度:

使用 bfcache 是在前後瀏覽時,加快網頁載入速度。

在影片中,使用 bfcache 的範例比沒有此範例的資訊稍快一些。

bfcache 不僅可加快瀏覽速度,還能減少數據用量,因為使用者不必再次下載資源。

Chrome 使用資料顯示,在電腦上進行瀏覽時,每 10 次有 10 次瀏覽,每 5 次就有 1 次是在行動裝置或前後進行。啟用 bfcache 後,瀏覽器每天就能消除資料傳輸,不需花費額外的時間載入數十億個網頁!

「快取」方式工作

「快取」bfcache 使用的號碼與 HTTP 快取不同,後者會發揮其本身作用,加快重複瀏覽的速度。bfcache 是記憶體中整個網頁的快照 (包括 JavaScript 堆積),而 HTTP 快取僅包含先前所提出要求的回應。由於需要從 HTTP 快取執行網頁載入的所有要求很少見,因此使用 bfcache 還原功能,重複造訪的速度都比是最針對非 bfcache 最最佳化的非快取瀏覽速度更快。

不過,在記憶體中建立頁面快照,會導致如何以最佳方式保留處理中的程式碼,變得較為複雜。舉例來說,當頁面位於 bfcache 的頁面發生逾時時,要如何處理 setTimeout() 呼叫?

答案是瀏覽器暫停所有待處理計時器或 bfcache 中的網頁未解決的承諾,包括 JavaScript 工作佇列中幾乎所有待處理的工作,如果網頁透過 bfcache 還原,則會恢復處理工作。

在某些案例中 (例如逾時和保證) 的風險很低,但在其他情況下可能會導致令人混淆或非預期的行為。例如,如果瀏覽器暫停了 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,以及在某些其他情況 (例如背景分頁凍結以盡量減少 CPU 用量),也會分派新的頁面生命週期事件 (包括 freezeresume)。只有以 Chromium 為基礎的瀏覽器支援這些事件。

透過 bfcache 還原頁面

pageshow 事件會在初次載入網頁及透過 bfcache 還原網頁時,立即觸發 load 事件。pageshow 事件具有 persisted 屬性,如果網頁是從 bfcache 還原,則為 falsetrue您可以使用 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 狀態

當網頁卸載,或瀏覽器嘗試將 bfcache 放入 bfcache 時,就會觸發 pagehide 事件。

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 中,即使網頁確實儲存在這裡,網頁也不會永遠保存在該網頁中。為了盡可能提高快取命中率,開發人員必須瞭解網頁符合資格 (且不符合資格) 須符合哪些條件。

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

一律不使用 unload 事件

如要在所有瀏覽器中針對 bfcache 進行最佳化,最重要的方法是一律不使用 unload 事件。永遠!

unload 事件會對瀏覽器造成問題,因為它會預先進行 bfcache 處理,且網際網路上的許多網頁會依合理假設 (合理) 判斷網頁在觸發 unload 事件後不會繼續存在。這會造成了上的困難,因為許多頁面「也」假設 unload 事件會在使用者離開網頁時觸發,但長期以來都傳回的結果

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

在電腦上,如果 Chrome 和 Firefox 會替網頁新增 unload 事件監聽器,這樣一來,網頁將無法使用 bfcache 功能,這會降低風險,且會使許多網頁失去資格。Safari 會嘗試透過 unload 事件監聽器快取部分網頁,但為了減少潛在中斷情形,在使用者離開時不會執行 unload 事件,導致事件非常不可靠。

在行動裝置上,Chrome 和 Safari 會嘗試快取含有 unload 事件監聽器的網頁,因為在行動裝置上執行 unload 事件總是不可靠,所以服務中斷的風險較低。Firefox 會將使用 unload 的網頁視為不符合 bfcache 的資格,但 iOS 系統要求所有瀏覽器都必須使用 WebKit 轉譯引擎,運作原理類似 Safari。

使用 pagehide 事件取代 unload 事件。pagehide 事件會在所有觸發 unload 事件的情況下觸發,而在頁面加入 bfcache 時也會觸發。

事實上,Lighthouse 提供 no-unload-listeners 稽核,如果開發人員網頁上任何 JavaScript (包括第三方程式庫) 新增了 unload 事件監聽器,就會警告開發人員。

基於穩定性和 bfcache 的效能影響,Chrome 打算淘汰 unload 事件

使用權限政策來避免在網頁上使用卸載處理常式

未使用 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 中。有鑑於我們正努力透過保護隱私權的方式變更 Chrome 行為,但目前使用 Cache-Control: no-store 的任何網頁都無法使用 bfcache 處理。

由於 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. 在開發人員工具中,前往「Application」->往返快取
  3. 按一下「Run Test」按鈕。開發人員工具會接著嘗試離開及返回 ,以判斷網頁是否能透過 bfcache 還原。
開發人員工具中的往返快取面板
開發人員工具中的「往返快取」面板。

如果測試成功,面板會回報「從往返快取還原」。

開發人員工具回報網頁已從 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 的轉送功能不使用 bfcache,這些情況包括:

  • 使用者關閉瀏覽器並再次啟動時
  • 使用者複製分頁時
  • 使用者關閉分頁再重新開啟時

在某些情況下,部分瀏覽器可能會保留原始導覽類型,因此即使這些瀏覽器不是往返導覽功能,還是可能會顯示一種 back_forward

即使沒有這些排除,bfcache 仍會在一段時間後遭到捨棄,以節省記憶體。

因此,網站擁有者不應預期在所有 back_forward 瀏覽動作中,都不應達到 100% 的 bfcache 命中率。不過,評估這類比率有助於找出網頁本身防止網頁以大量往返瀏覽比例使用 bfcache 的情形。

Chrome 團隊新增了 NotRestoredReasons API,以便顯示網頁不使用 bfcache 的原因,協助開發人員提高 bfcache 命中率。Chrome 團隊也在 CrUX 中新增導覽類型,以便在未自行評估的情況下查看 bfcache 導覽次數。

成效評估

bfcache 也會對欄位收集的成效指標 (尤其是會測量網頁載入時間的指標) 造成負面影響。

由於 Bfcache 瀏覽方式會還原現有網頁,而非啟動新的網頁載入,因此啟用 bfcache 之後,收集的網頁載入總數將會減少。但重點在於,如果網頁是以 bfcache 還原作業取代網頁載入,可能是資料集中載入速度最快的部分。這是因為往返瀏覽 (定義) 是重複造訪,且重複網頁載入的速度通常比首次訪客載入 (由於 HTTP 快取,如前所述) 的載入速度更快。

如此一來,儘管使用者感受到的效能可能有所提升,但該結果的頁面載入速度越快,各階段的分佈速度也可能會變慢。

以下提供幾個處理方式,以便解決這項問題。一種是為所有網頁載入指標加上各自的導覽類型註解:navigatereloadback_forwardprerender。讓您能繼續監控在這些導覽類型中的成效,即使整體分佈呈現負值的情況也不受影響。這種方法適用於非以使用者為主的網頁載入指標,例如首次位元組時間 (TTFB)

針對以使用者為中心的指標 (例如網站體驗核心指標),建議您記錄更能準確反映使用者體驗的值。

對 Core Web Vitals 的影響

Core Web Vitals 會在多種維度 (載入速度、互動性、視覺穩定性) 中評估使用者體驗。由於使用者體驗 Bfcache 的恢復速度,比完整頁面載入更快,因此 Core Web Vitals 指標也必須反映這一點。畢竟使用者不在意是否啟用 bfcache 問題,只是在註意導覽速度飛快的特性!

針對 Core Web Vitals 指標 (例如 Chrome 使用者體驗報告) 收集和回報的工具,會將 bfcache 還原作業視為資料集中獨立的網頁造訪記錄。此外,雖然沒有專屬的網路效能 API 用於在 bfcache 還原後評估這些指標,但您可以使用現有的網路 API 估算其值:

  • 如果是「Largest Contentful Paint (LCP)」,請使用 pageshow 事件的時間戳記與下一個繪製影格的時間戳記之間的差異,因為影格中的所有元素會同時進行繪製。如果是 bfcache 還原,則 LCP 和 FCP 相同。
  • 針對「與下一個顯示的內容互動 (INP)」,繼續使用現有的 Performance Observer,但將目前的 INP 值重設為 0。
  • 針對「累計版面配置位移 (CLS)」,請繼續使用現有的 Performance Observer,但將目前的 CLS 值重設為 0。

如要進一步瞭解 bfcache 對各項指標的影響,請參閱個別的 Core Web Vitals 指標指南頁面。如需這些指標實作 bfcache 版本的具體範例,請參閱 PR 將這些指標新增至 web-vitals JS 程式庫的具體範例。

web-vitals JavaScript 程式庫在回報的指標中支援 bfcache 還原

其他資源