往返快取

往返快取 (或 Bfcache) 是瀏覽器最佳化功能,可提供即時往返瀏覽。進而大幅改善瀏覽體驗,尤其對網路速度較慢或裝置的使用者而言更是如此。

身為網頁程式開發人員,請務必瞭解如何針對 bfcache 調整網頁,讓使用者從中獲得好處。

瀏覽器相容性

FirefoxSafari 早已支援 bfcache 的電腦和行動裝置使用者。

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

bfcache 基本概念

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

你是否常常造訪網站並點按連結前往其他頁面,卻只是意識到該網頁並不符合你的預期,因而點按返回按鈕?在這段期間,bfcache 改善前一頁的載入速度:

未啟用 bfcache 系統會發出新的要求來載入上一頁,且視該網頁已針對重複造訪進行 最佳化而定,瀏覽器可能需要重新下載、重新剖析及重新執行剛才下載的部分資源 (或全部)。
已啟用 bfcache 載入前一個網頁的動作基本上是立即載入,因為整個網頁可從記憶體中還原,無需前往網路。

請觀賞以下實際運作的 bfcache 實際運作影片,瞭解如何加快瀏覽網路的速度:

使用 bfcache 可以加快往返瀏覽作業的速度。

在影片中,使用 bfcache 的程式碼範例的速度比沒有 bfcache 的少一些。

bfcache 不僅能加快瀏覽速度,還能降低資料用量,因為不需要重新下載資源。

Chrome 使用資料顯示,在電腦上瀏覽 10 次時,每 10 次瀏覽中就有 1 頁是往返網頁。啟用 bfcache 後,瀏覽器就不用每天傳輸數十億個網頁,不用時常傳輸資料,也不用耗費時間載入數十億個網頁!

「快取」的運作方式

bfcache 使用的「快取」與 HTTP 快取不同,後者在加速重複瀏覽時扮演的角色。bfcache 是記憶體中整個網頁的快照,包括 JavaScript 堆積,而 HTTP 快取則只會包含先前提出要求的回應。從 HTTP 快取執行網頁所需的所有要求很少。因此,使用 bfcache 還原程序重複造訪的次數,甚至比大多數未經最佳化的非 bfcache 瀏覽作業更快。

不過,在記憶體中建立網頁的快照,會牽涉到最佳保存處理中程式碼的複雜度。舉例來說,當網頁位於 bfcache 時,要如何處理超過逾時的 setTimeout() 呼叫?

答案是,瀏覽器會暫停所有待處理的計時器,或對 bfcache 中網頁的未解決承諾 (包括 JavaScript 工作佇列中幾乎所有待處理工作),然後透過 bfcache 恢復網頁繼續處理工作。

在某些情況下 (例如逾時和承諾),這個風險就相當低,但在其他情況下則可能造成混淆或非預期的行為。舉例來說,如果瀏覽器暫停從事索引資料庫交易中的必要工作,可能會影響相同來源中其他開啟的分頁,因為相同的索引資料庫資料庫可同時由多個分頁存取。因此,瀏覽器通常不會嘗試在 IndexedDB 交易期間快取網頁,也不會在您使用可能影響其他網頁的 API 時嘗試快取網頁。

如要進一步瞭解各種 API 使用情形如何影響網頁的 bfcache 資格,請參閱「針對 bfcache 改善網頁」。

bfcache 和 iframe

如果網頁含有嵌入式 iframe, iframe 本身不適用這種圖片。舉例來說,如果您前往一個 iframe 內的其他網頁,然後返回,瀏覽器會回到 iframe 中的「返回」頁面,而不是在主頁框中,但 iframe 內的返回導覽不會使用 bfcache。

此外,如果嵌入的 iframe 使用封鎖 bfcache 的 API,系統也會禁止主頁框使用 bfcache。如要避免這種情況發生,您可以在主頁框中設定的權限政策,或是使用 sandbox 屬性

bfcache 與單頁應用程式 (SPA)

bfcache 可與瀏覽器管理的導覽搭配運作,因此無法用於單頁應用程式 (SPA) 中的「軟導覽」。不過,bfcache 在返回 SPA 後仍能提供協助,而非從頭開始將該應用程式完全重新初始化。

用來觀察 bfcache 的 API

雖然 bfcache 是瀏覽器會自動執行的最佳化作業,但開發人員仍必須瞭解網站發生的時間,以便針對網頁進行最佳化調整,並據此調整任何指標或成效評估

用於觀察 bfcache 的主要事件是網頁轉換事件 pageshowpagehide多數瀏覽器都支援這類事件。

頁面進入或離開 bfcache,在其他情況下 (例如背景分頁凍結以盡量減少 CPU 使用率) 也會分派較新的「Page Lifecycle」事件 (即 freezeresume)。這些事件僅適用於以 Chromium 為基礎的瀏覽器。

觀察網頁何時透過 Bfcache 還原

網頁初次載入或從 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 對數據分析和效能評估的影響

觀察網頁何時進入 bfcache

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

pagehide 事件也具有 persisted 屬性。如果狀態是 false,那麼請放心,該網頁可能不會進入 bfcache。不過,persistedtrue 並不保證一定會快取網頁。這表示瀏覽器「預期」intends要快取網頁,但可能還有其他因素導致網頁無法快取。

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

同樣地,如果 persistedtrue,則會在 pagehide 事件之後立即觸發 freeze 事件,但這只是表示瀏覽器「希望」intends快取網頁。稍後仍可能基於一些原因而捨棄它。

針對網頁快取進行網頁最佳化

並非所有網頁都會儲存在 bfcache 中,即使網頁已經儲存在快取中,也不會無限期保留。開發人員必須瞭解網頁符合資格 (或不符合資格) 的原因,以便盡可能提高快取命中率。

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

永不使用「unload」事件

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

unload 事件會對瀏覽器造成問題,因為該事件會預先快取,且許多網際網路上的網頁在執行 (合理的) 假設在 unload 事件觸發後繼續存在。這是一項挑戰,因為許多網頁都假設 unload 事件會在使用者離開時觸發,而此事件不再真實 (且長時間沒有真實狀態)。

瀏覽器目前面臨兩難的困境,他們必須擇一改善使用者體驗,但也可能破壞網頁。

在電腦上,Chrome 和 Firefox 會選擇加入 unload 事件監聽器,使其無法載入網頁快取,這不但風險較低,也不符合許多網頁的資格。Safari 會嘗試使用 unload 事件監聽器快取部分網頁,但為了減少潛在中斷問題,不會在使用者離開時執行 unload 事件,導致事件非常不可靠。

在行動裝置上,Chrome 和 Safari 會嘗試使用 unload 事件監聽器快取網頁,因為發生中斷風險的風險更低,因為 unload 事件在行動裝置上一直以來非常不可靠。Firefox 會將使用 unload 的網頁視為不適用 bfcache;在 iOS 上則需要所有瀏覽器都必須使用 WebKit 轉譯引擎,因此運作方式類似 Safari。

請改用 pagehide 事件,不要使用 unload 事件。每當 unload 事件觸發時,就會觸發 pagehide 事件,而也會「也會」在網頁放入 bfcache 時觸發。

事實上,Lighthouse 有一項 no-unload-listeners 稽核,可在網頁中的任何 JavaScript (包括第三方程式庫) 新增 unload 事件監聽器時警告開發人員。

由於擴充功能的穩定性及對 bfcache 的效能影響,Chrome 即將淘汰 unload 事件

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

如果網站未使用 unload 事件處理常式,可以使用 Chrome 115 的權限政策,確保不會新增這些事件。

Permission-Policy: unload()

此外,這麼做也能新增卸載處理常式,讓網站無法使用快取功能,防止第三方或擴充功能拖慢網站運作速度。

只在有條件的情況下新增 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 快取中。用於含有敏感使用者資訊的資源,例如需要登入才能瀏覽的網頁。

雖然 bfcache 不是 HTTP 快取,但是一直以來,在網頁資源本身 (而非任何子資源) 上設定 Cache-Control: no-store 時,瀏覽器並未選擇將網頁儲存在 bfcache。目前您可以透過能夠保護隱私權的方式變更這項 Chrome 行為,但目前使用 Cache-Control: no-store 的所有網頁都不適用往返快取。

由於 Cache-Control: no-store 會限制網頁是否適用 bfcache 的規定,因此請只在內含機密資訊的網頁上設定這項設定,因為任何類型都不適合快取。

如果網頁需要隨時提供最新內容,且其中不含機密資訊,請使用 Cache-Control: no-cacheCache-Control: max-age=0。這些指令會指示瀏覽器在放送內容前重新驗證內容,也不會影響網頁是否可使用快取。

請注意,透過 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 在各個往返瀏覽動作上放送一組新的廣告。然而,除了對成效的影響外,也不必擔心這種行為能否提升廣告參與度。使用者或許已註意到想要返回點擊的廣告,但卻重新載入,而不是從無法用的快取中還原。在做出假設之前,請務必測試這個情境 (最好使用 A/B 測試)。

如果網站需要重新整理 bfcache 還原廣告,然後在 event.persistedtrue 時僅重新整理 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),請暫停這些工作,但不會因使用者無法瀏覽網頁而造成任何問題。

不過,如果這些工作已連結至可透過相同來源的其他網頁 (例如 IndexedDB、Web Locks、WebSockets) 存取的 API,就會造成這個問題,因為暫停這些工作可能會導致其他分頁中的程式碼無法運作。

因此,在下列情況下,部分瀏覽器不會嘗試將網頁放在 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 開發人員工具可以協助你測試網頁,確保網頁經過最佳化調整,並能找出導致網頁不符資格的問題。

如何測試網頁:

  1. 在 Chrome 中前往該網頁。
  2. 在開發人員工具中,前往「應用程式」->「往返快取」
  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 還原,請設定 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 導覽作業都能達到 100% 的 bfcache 命中率。不過,透過評估網頁比例,您可以找出網頁本身禁止高比例的往返瀏覽,使用 Bfcache 的網頁。

Chrome 團隊新增了 NotRestoredReasons API,以說明網頁未使用 bfcache 的原因,以便開發人員改善 bfcache 命中率。Chrome 團隊也在 CrUX 新增了導覽類型,這樣即使不自行測量,也能查看 bfcache 瀏覽的數量。

成效評估

bfcache 也會對實際欄位收集的成效指標造成負面影響,特別是評估網頁載入時間的指標。

由於 bfcache 瀏覽程序會還原現有網頁,而非啟動新的網頁載入,因此啟用 bfcache 後,收集到的網頁載入總數會減少。然而最重要的是,用 bfcache 還原功能取代網頁載入或許是資料集中最快的網頁載入。這是因為根據定義,往返瀏覽是指重複瀏覽,而重複載入網頁的速度,通常比首次訪客載入頁面的速度更快 (如前文所述,HTTP 快取的緣故)。

以結果顯示,資料集中的頁面載入速度越快,分佈速度可能會因此變慢,儘管使用者表現出的體驗可能有所提升!

有幾種方法可以解決這個問題,一種是為所有網頁載入指標加上註解,個別的瀏覽類型navigatereloadback_forwardprerender。這樣一來,您就能持續監控這些導覽類型的成效,即使整體分佈有負面情況也是如此。適合非以使用者為中心的網頁載入指標,例如第一個位元組 (TTFB)

如果是網站體驗核心指標等以使用者為中心的指標,更好的做法是回報更準確反映使用者體驗的值。

對網站體驗核心指標的影響

Core Web Vitals 指標會從各種維度 (載入速度、互動性、視覺穩定性) 評估使用者的網頁體驗。使用者看到的 bfcache 還原體驗比完整頁面載入速度更快,因此網站體驗核心指標的指標必須反映這一點。畢竟,使用者不在意是否啟用 bfcache 了,只是關心瀏覽速度很快!

這類工具會收集網站體驗核心指標報告 (例如 Chrome 使用者體驗報告) 並製作相關報表,將 bfcache 還原作業視為資料集中的個別網頁造訪次數。在 bfcache 還原後,雖然沒有專屬的網路效能 API 可用於評估這些指標,但您可以使用現有的網路 API 來估算這些指標的值:

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

如要進一步瞭解 bfcache 對每項指標的影響,請參閱個別網站體驗核心指標的指標指南頁面。如需相關範例,瞭解如何實作這些指標的 bfcache 版本,請參閱「將指標新增至 web-vitals JS 程式庫的 PR」。

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

其他資源