バックフォワード キャッシュ(bfcache)は、すぐに前後に移動できるブラウザの最適化です。これにより、特に低速のネットワークやデバイスを使用するユーザーのブラウジング エクスペリエンスが大幅に向上します。
このページでは、すべてのブラウザで bfcache を使用するようにページを最適化する方法について説明します。
ブラウザの互換性
bfcache は、パソコンとモバイルの両方で、長年にわたって Firefox と Safari の両方でサポートされています。
Chrome バージョン 86 以降では、ごく一部のユーザーを対象に、Android のクロスサイト ナビゲーション用に bfcache が有効になっています。今後のリリースでは、追加のサポートが段階的にリリースされます。バージョン 96 以降では、パソコンとモバイルを使用するすべての Chrome ユーザーに対して 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 のモニタリングに使用される主なイベントは、ページ遷移イベントの pageshow
と pagehide
です。これらはほとんどのブラウザでサポートされています。
新しいページのライフサイクル イベントである freeze
と resume
は、ページが 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 に入ろうとしていると確信できます。ただし、persisted
が true
であっても、ページがキャッシュされるとは限りません。これは、ブラウザがページをキャッシュしようとすることを意味しますが、他の要因によってキャッシュできない場合もあります。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.');
}
});
同様に、persisted
が true
の場合、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
が使用されているかどうかを確認するには、Lighthouse の no-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.persisted
が true
の場合に、常に 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.persisted
が true
のときに 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 に追加しようとしません。
- 開いている IndexedDB 接続
- 進行中の fetch() または XMLHttpRequest
- オープンな WebSocket または WebRTC 接続
ページでこれらの 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 の対象にならない可能性のある問題を特定できます。
ページをテストするには:
- Chrome のページに移動します。
- DevTools で、[Application] > [バックフォワード キャッシュ] に移動します。
- [Run Test] ボタンをクリックします。その後 DevTools は、そのページを離れてから戻って、bfcache からページを復元できるかどうかを判断しようとします。
テストが成功すると、パネルに「Restored from back-forward cache」と表示されます。失敗した場合は、その理由がパネルに表示されます。理由の一覧については、Chromium が復元されなかった理由リストをご覧ください。
デベロッパーが対処できる理由については、パネルに [Actionable](対応可能)とマークされます。
この画像では、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 つは、すべてのページ読み込み指標に、それぞれのナビゲーション タイプ(navigate
、reload
、back_forward
、prerender
)でアノテーションを付ける方法です。これにより、全体的な分布が負に偏っている場合でも、これらのナビゲーション タイプ内でパフォーマンスを引き続きモニタリングできます。最初のバイトまでの時間(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 ライブラリへの追加をご覧ください。
参考情報
- Firefox キャッシュ (Firefox では bfcache)
- ページ キャッシュ (Safari の bfcache)
- バックフォワード キャッシュ: ウェブで公開される動作(ブラウザ間の bfcache の違い)
- bfcache テスター (さまざまな API とイベントがブラウザの bfcache にどのように影響するかをテスト)
- パフォーマンス ゲーム チェンジャー: ブラウザのバックフォワード キャッシュ (Smashing Magazine の事例で、bfcache を有効にすることで Core Web Vitals が劇的に改善されました)