往返快取

發布日期:2023 年 5 月 25 日,上次更新日期:2025 年 3 月 25 日

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

身為網頁開發人員,瞭解如何為 bfcache 最佳化網頁至關重要,這樣使用者才能享有相關優勢。

瀏覽器相容性

所有主要瀏覽器都包含 bfcache,包括 Chrome (自 96 版起)、FirefoxSafari

往返快取基本概念

有了往返快取 (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 仍可派上用場,不必從頭重新完整初始化該應用程式。

用於觀察 bfcache 的 API

雖然瀏覽器會自動執行 bfcache 最佳化,但開發人員仍須瞭解何時會發生這種情況,才能針對 bfcache 最佳化網頁,並據此調整任何指標或成效評估

觀察 bfcache 的主要事件是網頁轉場事件 pageshowpagehide大多數瀏覽器都支援這些事件。

當網頁進入或離開 bfcache,以及在其他情況下 (例如背景分頁遭凍結以減少 CPU 使用量),系統也會傳送較新的「網頁生命週期」事件 (freezeresume)。這些事件僅適用於以 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

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

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

同樣地,如果 persistedtruefreeze 事件會在 pagehide 事件後立即觸發,但這只表示瀏覽器打算快取網頁。但基於稍後說明的多項原因,系統可能仍須捨棄該要求。

針對 bfcache 最佳化網頁

並非所有網頁都會儲存在往返快取中,即使網頁儲存在往返快取中,也不會無限期保留。開發人員務必瞭解網頁是否符合 bfcache 資格,才能盡可能提高快取命中率。

以下各節將說明最佳做法,盡可能讓瀏覽器快取網頁。

請勿使用 unload 事件

在所有瀏覽器中,最佳化 bfcache 最重要的方法就是絕不使用 unload 事件。Ever!

對瀏覽器來說,unload 事件是個問題,因為這個事件早於 bfcache,且網路上許多網頁都 (合理地) 假設網頁在觸發 unload 事件後就不會繼續存在。這是一項挑戰,因為許多網頁是根據使用者離開時會觸發 unload 事件的假設所建構,但這已不再成立 (事實上,這項假設已不成立很長一段時間)。

因此瀏覽器面臨兩難,必須在改善使用者體驗和網頁可能無法正常運作之間做出選擇。

在桌機上,Chrome 和 Firefox 選擇在網頁新增 unload 監聽器時,讓網頁不符合 bfcache 資格,雖然風險較低,但也會導致許多網頁不符合資格。Safari 會嘗試快取部分含有 unload 事件監聽器的網頁,但為了減少潛在的損壞,當使用者離開時,系統不會執行 unload 事件,因此這個事件非常不可靠。

在行動裝置上,Chrome 和 Safari 會嘗試快取含有 unload 事件監聽器的網頁,因為 unload 事件在行動裝置上一直極不可靠,因此中斷的風險較低。Firefox 會將使用 unload 的網頁視為不符合 bfcache 資格,但 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-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,如果沒有,就會重新載入:

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 發布商廣告代碼執行這項操作

避免使用 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:

如果網頁使用上述任一 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. 在開發人員工具中,依序前往「應用程式」->「返回/快取」
  3. 按一下「Run Test」按鈕。開發人員工具接著會嘗試離開並返回,判斷網頁是否可從 bfcache 還原。
開發人員工具中的往返快取面板
開發人員工具中的「往返快取」面板。

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

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

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

開發人員工具回報無法從往返快取還原網頁
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.
// This happens by default just by loading gtag
gtag('config', 'TAG_ID');

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.
// To do this disable the default pageview so you can manually send it
// supplemented with the additional detail.
gtag('config', 'TAG_ID', { send_page_view: false });
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 快取)。

因此資料集中快速載入的網頁會減少,即使使用者體驗可能有所提升,但分配速度仍可能變慢!

有幾種方法可以解決這個問題,方法之一是使用各自的導覽類型 (navigatereloadback_forwardprerender) 註解所有載入網頁指標。即使整體分配比例偏向負面,您仍可繼續監控這些導覽類型的成效。如果是「Time to First Byte (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

其他資源