バックフォワード キャッシュ

バックフォワード キャッシュ(bfcache)は、すぐに前後に移動できるブラウザの最適化です。これにより、特に低速のネットワークやデバイスを使用するユーザーのブラウジング エクスペリエンスが大幅に向上します。

このページでは、すべてのブラウザで bfcache を使用するようにページを最適化する方法について説明します。

ブラウザの互換性

bfcache は、パソコンとモバイルの両方で、長年にわたって FirefoxSafari の両方でサポートされています。

Chrome バージョン 86 以降では、ごく一部のユーザーを対象に、Android のクロスサイト ナビゲーション用に bfcache が有効になっています。今後のリリースでは、追加のサポートが段階的にリリースされます。バージョン 96 以降では、パソコンとモバイルを使用するすべての Chrome ユーザーに対して bfcache が有効になっています。

bfcache の基本

bfcache は、ユーザーが離れたページの完全なスナップショットを保存するメモリ内キャッシュです。ブラウザはページ全体をメモリ内に保持することで、ページを読み込むのに必要なネットワーク リクエストをすべて繰り返す必要がなく、ユーザーがページに戻ることを決断した場合にすばやくページを復元できます。

次の動画では、bfcache によってナビゲーションがどの程度高速化されるかを示しています。

bfcache を使用すると、「戻る」や「進む」ナビゲーション中のページの読み込み速度が大幅に上がります。

Chrome の使用状況データによると、パソコンでのナビゲーションの 10 分の 1、モバイルでの 5 分の 1 は、「戻る」または「進む」です。このため、bfcache には時間とデータ使用量を大幅に節約できる可能性があります。

「キャッシュ」の仕組み

bfcache で使用される「キャッシュ」は、HTTP キャッシュとは異なります。HTTP キャッシュは、メモリ内のページ全体(JavaScript ヒープを含む)のスナップショットです。これに対して、HTTP キャッシュには以前に行われたリクエストに対するレスポンスのみが含まれます。HTTP キャッシュからページを読み込む必要があるすべてのリクエストが HTTP キャッシュから処理されることはまれであるため、bfcache 復元を使用した繰り返しアクセスは、最適化された bfcache 以外のナビゲーションよりも常に高速になります。

ただし、メモリ内にページのスナップショットを作成する場合、進行中のコードの最適な保存方法に関して多少の複雑さが伴います。たとえば、ページが bfcache にある間にタイムアウトに達した setTimeout() 呼び出しをどのように処理しますか。

その答えは、ブラウザは bfcache 内のページに対する保留中のタイマーや未解決の Promise(JavaScript タスクキュー内のほぼすべての保留中のタスクを含む)を一時停止し、ページが bfcache から復元されると処理タスクを再開することです。

タイムアウトや Promise など、リスクが非常に低い場合もありますが、混乱や予期しない動作につながる場合もあります。たとえば、IndexedDB トランザクションに必要なタスクをブラウザが一時停止すると、同じ IndexedDB データベースに同時に複数のタブからアクセスできるため、同じオリジンで開いている他のタブに影響する可能性があります。そのため、ブラウザは通常、IndexedDB トランザクションの途中や、他のページに影響する可能性のある API を使用している間はページをキャッシュに保存しません。

API の使用状況がページの bfcache の利用資格に与える影響について詳しくは、bfcache 用にページを最適化するをご覧ください。

bfcache とシングルページ アプリ(SPA)

bfcache はブラウザが管理するナビゲーションで動作するため、シングルページ アプリ(SPA)内の「ソフト ナビゲーション」では機能しません。ただし、bfcache は SPA を離れるときと戻るときに役立ちます。

bfcache を監視するための API

bfcache はブラウザによって自動的に行われる最適化ですが、デベロッパーが bfcache に基づいてページを最適化し、それに応じて指標やパフォーマンス測定を調整できるように、bfcache がいつ行われるかを把握することは重要です。

bfcache のモニタリングに使用される主なイベントは、ページ遷移イベントpageshowpagehide です。これらはほとんどのブラウザでサポートされています。

新しいページのライフサイクル イベントである freezeresume は、ページが bfcache に出入りするとき、およびその他の状況(CPU 使用率を最小限に抑えるためにバックグラウンドのタブがフリーズした場合など)でもディスパッチされます。これらのイベントは、Chromium ベースのブラウザでのみサポートされています。

ページが bfcache から復元されたことを確認する

pageshow イベントは、ページが最初に読み込まれるとき、およびページが bfcache から復元されたときに、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 であっても、ページがキャッシュされるとは限りません。これは、ブラウザがページをキャッシュしようとすることを意味しますが、他の要因によってキャッシュできない場合もあります。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 の場合、freeze イベントは pagehide イベントの直後に発生しますが、これはブラウザがページをキャッシュに保存する意図があることを意味します。intends後述するさまざまな理由で、さらにデータを破棄しなければならない場合があります。

bfcache 用にページを最適化する

すべてのページが bfcache に保存されるわけではなく、ページが bfcache に保存されるわけでも、無期限に保持されるわけではありません。以下のページでは、ページが bfcache の対象になる理由の概要と、ブラウザでページをキャッシュに保存してキャッシュ ヒット率を高めるためのベスト プラクティスを紹介します。

unload ではなく pagehide を使用する

すべてのブラウザで bfcache に合わせて最適化する最も重要な方法は、unload イベント リスナーを使用しないことです。代わりに pagehide をリッスンしてください。これは、ページが bfcache に追加されたときと、unload が呼び出されたときに呼び出されるためです。

unload は古い機能で、元々はユーザーがページから移動するたびにトリガーされるように設計されていました。これは現在は当てはまりませんが、多くのウェブページは依然として、ブラウザがこの方法で unload を使用し、unload がトリガーされた後に、読み込み解除されたページが存在しなくなることを前提として動作しています。これにより、読み込まれていないページをブラウザがキャッシュに保存しようとすると、bfcache が破損する可能性があります。

パソコンの場合、Chrome と Firefox で unload リスナーを含むページは bfcache の対象外になります。これによりリスクは軽減されますが、多くのページがキャッシュに保存されず、再読み込みに時間がかかります。Safari は unload イベント リスナーを使用して一部のページをキャッシュに保存しようとしますが、破損の可能性を減らすために、ユーザーがページから移動したときに unload イベントが実行されないため、unload リスナーの信頼性が低下します。

モバイルでは、Chrome と Safari は unload イベント リスナーを使用してページをキャッシュに保存しようとします。これは、モバイル上での unload の信頼性が低いと、破損のリスクが低下するためです。モバイル版 Firefox は、unload を使用するページを bfcache の対象外として扱います。ただし、すべてのブラウザで WebKit レンダリング エンジンを使用する必要がある iOS は除く、Safari のように動作します。

ページ上の JavaScript に unload が使用されているかどうかを確認するには、Lighthouseno-unload-listeners 監査を使用することをおすすめします。

Chrome における unload のサポート終了については、アンロード イベントのサポート終了をご覧ください。

権限ポリシーを使用して、ページでアンロード ハンドラが使用されないようにする

サードパーティのスクリプトや拡張機能によっては、アンロード ハンドラがページに追加され、bfcache の対象外となることでサイトの処理速度が低下する場合があります。Chrome 115 以降でこれを防ぐには、権限ポリシーを使用します。

Permission-Policy: unload()

条件付きでのみ beforeunload リスナーを追加する

beforeunload イベントが発生しても、ページが bfcache の対象から外れることはありません。 ただし、この方法は信頼性が低いため、どうしても必要な場合にのみ使用することをおすすめします。

beforeunload のユースケースの一例として、保存していない変更内容はページから移動すると失われることをユーザーに警告することが考えられます。この場合、ユーザーの変更が保存されていない場合にのみ 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);
});

Cache-Control: no-store の使用を最小限に抑える

Cache-Control: no-store は、ウェブサーバーがレスポンスに対して設定できる HTTP ヘッダーで、HTTP キャッシュに保存しないようにブラウザに指示します。ログインが必要なページなど、ユーザーの機密情報を含むリソースに使用されます。

bfcache は HTTP キャッシュではありませんが、ページリソースに(サブリソースではなく)Cache-Control: no-store が設定されている場合、ブラウザでは bfcache からページを除外していました。Chrome では、ユーザーのプライバシーを保護しながらこの動作を変更しています。ただし、デフォルトでは Cache-Control: no-store を使用しているページは bfcache の対象になりません。

bfcache に合わせて最適化するには、キャッシュに保存すべきでない機密情報を含むページにのみ Cache-Control: no-store を使用します。

常に最新のコンテンツを提供するものの、機密情報を含まないページの場合は、Cache-Control: no-cache または Cache-Control: max-age=0 を使用します。コンテンツを配信する前に再検証するようブラウザに指示します。bfcache からページを復元しても HTTP キャッシュは含まれないため、ページの 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 の参照を避ける

古いブラウザでは、rel="noopener" を指定せずに target=_blank でリンクから window.open() を使用してページを開いた場合、開始ページは開いているページの window オブジェクトへの参照を持ちます。

null 以外の window.opener 参照があるページは、セキュリティ上のリスクになるだけでなく、bfcache に追加することもできません。これは、アクセスしようとするページが失敗する可能性があるためです。

これらのリスクを回避するには、rel="noopener" を使用して window.opener 参照が作成されないようにします。これは、最新のすべてのブラウザでデフォルトの動作です。 サイトで window.postMessage() を使用するか、window オブジェクトを直接参照してウィンドウを開いて制御する必要がある場合、開いているウィンドウもオープナーも bfcache の対象になりません。

ユーザーが離れる前に、開いている接続を閉じる

前述のように、ページが bfcache に入ると、スケジュールされたすべての JavaScript タスクを一時停止し、ページがキャッシュから取り出された時点で再開します。

これらのスケジュールされた JavaScript タスクが、DOM API または現在のページに分離された他の API のみにアクセスする場合、ページがユーザーに表示されないときにこれらのタスクを一時停止しても問題は発生しません。

ただし、これらのタスクが、同じオリジンの他のページからもアクセスできる API(IndexedDB、Web Lock、WebSocket など)に接続されている場合、タスクを一時停止すると、それらのページのコードが実行されなくなるため、それらのページが破損する可能性があります。

そのため一部のブラウザでは、次のいずれかに該当するページを bfcache に追加しようとしません。

ページでこれらの API のいずれかを使用している場合は、pagehide イベントまたは freeze イベント中に接続を閉じ、オブザーバーを削除するか切断することを強くおすすめします。これにより、ブラウザは他の開いているタブに影響を与えることなく、ページを安全にキャッシュできます。その後、ページが bfcache から復元されると、pageshow イベントまたは resume イベント中にそれらの 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 DevTools では、ページが bfcache 用に最適化されているかどうかをテストして確認し、bfcache の対象にならない可能性のある問題を特定できます。

ページをテストするには:

  1. Chrome のページに移動します。
  2. DevTools で、[Application] > [バックフォワード キャッシュ] に移動します。
  3. [Run Test] ボタンをクリックします。その後 DevTools は、そのページを離れてから戻って、bfcache からページを復元できるかどうかを判断しようとします。
DevTools のバックフォワード キャッシュ パネル
DevTools の [バックフォワード キャッシュ] パネル

テストが成功すると、パネルに「Restored from back-forward cache」と表示されます。失敗した場合は、その理由がパネルに表示されます。理由の一覧については、Chromium が復元されなかった理由リストをご覧ください。

デベロッパーが対処できる理由については、パネルに [Actionable](対応可能)とマークされます。

DevTools で 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 アナリティクスでこれを行う方法を示しています。他の分析ツールも同様のロジックを使用する可能性があります。

// 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 を使用していないページを特定するには、次のようにページ読み込みのナビゲーション タイプを測定します。

// 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 チームは、ページで bfcache が使用されない理由を明らかにし、デベロッパーが bfcache のヒット率を改善できるように、NotRestoredReasons API に取り組んでいます。

パフォーマンスの測定

bfcache は、フィールドで収集されたパフォーマンス指標、特にページの読み込み時間を測定する指標にも悪影響を及ぼす可能性があります。

bfcache のナビゲーションは、新しいページの読み込みを開始するのではなく既存のページを復元するため、bfcache を有効にすると、収集されるページ読み込みの合計数が少なくなります。ただし、bfcache による置換は、データセット内で最も高速なページ読み込みの 1 つと考えられます。「戻る」ナビゲーションと「フォワード ナビゲーション」などの繰り返しページ読み込みと、HTTP キャッシュのおかげで初回ページ読み込みよりも速いためです。そのため、bfcache を有効にすると、ユーザーにとってはサイトのパフォーマンスは向上するにもかかわらず、アナリティクスではページの読み込みが遅くなる可能性があります。

この問題にはいくつかの方法で対処できます。1 つは、すべてのページ読み込み指標に、それぞれのナビゲーション タイプnavigatereloadback_forwardprerender)でアノテーションを付ける方法です。これにより、全体的な分布が負に偏っている場合でも、これらのナビゲーション タイプ内でパフォーマンスを引き続きモニタリングできます。最初のバイトまでの時間(TTFB)など、ユーザー中心ではないページ読み込み指標には、この方法をおすすめします。

Core Web Vitals などのユーザー中心の指標の場合は、ユーザー エクスペリエンスをより正確に表す値を報告する方が適切です。

Core Web Vitals への影響

Core Web Vitals は、さまざまな項目(読み込み速度、インタラクティビティ、視覚的な安定性)でウェブページのユーザー エクスペリエンスを測定します。Core Web Vitals の指標には、デフォルトのページ読み込みよりも高速なナビゲーションとしてユーザーが bfcache 復元を行うという事実を反映することが重要です。

Chrome ユーザー エクスペリエンス レポートなど、Core Web Vitals の指標を収集して報告するツールは、bfcache による復元をデータセット内の個別のページアクセスとして処理します。また、bfcache の復元後にこれらの指標を測定するための専用のウェブ パフォーマンス API はありませんが、既存のウェブ API を使用して値を近似できます。

  • Largest Contentful Paint(LCP)では、pageshow イベントのタイムスタンプと次のペイント フレームのタイムスタンプの差分を使用します。これは、フレーム内のすべての要素が同時にペイントされるためです。bfcache 復元の場合、LCP と FCP は同じです。
  • [Interaction to Next Paint(INP)] では、既存の Performance Observer を引き続き使用し、現在の CLS 値を 0 にリセットします。
  • Cumulative Layout Shift(CLS)では、既存の Performance Observer を引き続き使用し、現在の CLS 値を 0 にリセットします。

bfcache が各指標に与える影響の詳細については、Core Web Vitals の指標ガイドのページをご覧ください。これらの指標の bfcache バージョンを実装する方法の具体例については、PR による web-vitals JS ライブラリへの追加をご覧ください。

参考情報