Back/Forward Cache(bfcache)の保存条件と復元
戻る・進むが一瞬で表示される理由が分かる。ページをまるごとメモリに凍結して即復元する仕組み、bfcache を阻害する unload や開いた接続、pageshow/pagehide の使い方を内部から正確に解きます。
- 1.bfcache はページを破棄せず、DOM・JS ヒープ・実行状態ごとメモリに凍結する。戻る/進む時はネットワークも再パースもなくスナップショットを復元するため、表示は数十ミリ秒で完了する。
- 2.unload イベントの登録、Cache-Control: no-store、開いたままの WebSocket/IndexedDB トランザクション、許可ダイアログ表示中などは bfcache 入りを阻害する。pagehide の event.persisted で凍結されたか判定できる。
- 3.復元時は load ではなく pageshow(event.persisted === true)が発火する。凍結前に切るべき接続は pagehide で閉じ、復元時に再開する設計にすると、戻る操作の体感速度を最大化できる。
なぜ bfcache を正確に理解する必要があるのか
「戻る」ボタンを押したとき、ページがネットワーク往復もスクロール位置のリセットもなく一瞬で出てくることがあります。これを実現しているのが Back/Forward Cache(bfcache、往復キャッシュ) です。通常のナビゲーションはページを破棄して次のページを一から組み立てますが、bfcache はページを破棄せずメモリに凍結しておき、戻る/進むのときにその凍結状態をそのまま蘇らせます。問題は、この凍結が条件を1つでも満たさないと働かない点です。条件は仕様アルゴリズムとして定義されており、知らずに unload を登録したり接続を開きっぱなしにすると bfcache が黙って無効化され、戻るが遅くなります。本記事はその保存条件と復元イベントを内部レベルで解きほぐします。
凍結とは何か:破棄しないという発想
通常、別ページへ遷移すると、現在のページのドキュメントは破棄され、JS の実行コンテキスト・DOM ツリー・タイマー・ヒープがすべて解放されます。戻ってきたときは再度 HTML を取得しパースし直すため、最良でもネットワーク往復+再構築のコストがかかります。
bfcache はこの破棄を行いません。遷移時にページ全体を frozen(凍結) 状態にして、次の構成要素をメモリ上にそのまま保持します。
| 保持されるもの | 凍結中の扱い |
|---|---|
| DOM ツリー | そのままメモリに残る(再パース不要) |
| JS ヒープ・変数・クロージャ | 値を保ったまま凍結。グローバル状態も維持 |
| タイマー・実行中の処理 | 一時停止。setTimeout/requestAnimationFrame は止まる |
| スクロール位置・フォーム入力 | そのまま保存され復元される |
| ネットワーク接続(fetch 等) | 進行中だと凍結を阻害する要因になる |
凍結されたページは JS が走らない静止状態に置かれます。タイマーのコールバックも、進行中の Promise の続きも、復元されるまで保留されます。だからこそ復元は速い――ネットワークも再パースもなく、保留していた実行を再開してスクロール位置を当てるだけで表示が完了するため、戻る操作は典型的に数十ミリ秒で終わります。この破棄しない設計が、通常ナビゲーションとの決定的な違いです。
bfcache は HTTP キャッシュ と混同されがちですが別物です。HTTP キャッシュは「取得したレスポンス(バイト列)」を再利用してネットワークを省くもので、再利用時はそのバイト列から DOM を作り直します。bfcache が保存するのは組み上がった後の生きたページ状態そのものです。後者は再構築すら不要なので、同じ「戻る」でも速度の次元が違います。
何が bfcache を阻害するのか
ブラウザはページを凍結できるかどうかを、遷移の瞬間に適格性(eligibility)チェックで判定します。1つでも該当すると凍結を諦め、通常どおりページを破棄します。代表的な阻害要因は次のとおりです。
| 阻害要因 | なぜ阻害するか |
|---|---|
| unload イベントの登録 | 凍結との両立が困難で、多くのブラウザが一律に非適格化する |
| Cache-Control: no-store | 保存禁止指定のため、機微なページを凍結しない |
| 開いた WebSocket / WebRTC | 凍結中に接続を維持できず、切るかページ破棄かになる |
| 進行中の IndexedDB トランザクション | 未完了の処理を凍結すると整合性が崩れるため非適格 |
| 権限ダイアログ等の表示中 | ユーザー操作待ちの状態は凍結できない |
| window.opener が生きた状態 | 他文書から参照され続けるため安全に凍結できない場合がある |
最も多い落とし穴が unload イベントです。歴史的に「離脱時にデータ送信」目的で使われてきましたが、unload の存在自体が bfcache を無効化します。unload はそもそも信頼性が低く(モバイルでは発火しないことがある)、代替として pagehide や visibilitychange を使うのが現在の定石です。beforeunload も一部ブラウザで阻害要因となるため、登録は遷移の確認が本当に必要な場面に限定します。
レスポンスヘッダに Cache-Control: no-store を付けると、そのページは bfcache 対象外になります。ログイン後のページなどで反射的に no-store を付けがちですが、bfcache を効かせたい画面では no-cache(再検証は必須だが保存は許可)や、必要な機微部分だけを別扱いにする設計を検討します。「保存させない」と「凍結させない」は別の目的なので、混同するとパフォーマンスを無駄に落とします。
開いた接続(WebSocket や進行中の fetch、IndexedDB トランザクション)は、凍結中に維持できないため阻害要因になります。これらは後述の pagehide で能動的に閉じ、復元時に張り直す設計にすれば、凍結を妨げずに済みます。
pagehide / pageshow:凍結と復元のフック
bfcache を扱う鍵は、ライフサイクルイベントを load/unload ではなく pageshow/pagehide で捉えることです。両イベントは event.persisted というブール値を持ち、これが凍結が絡んだ遷移かどうかを示します。
| イベント | 発火タイミング | event.persisted |
|---|---|---|
| pageshow | ページ表示時(初回ロード/bfcache 復元の両方) | true なら bfcache から復元 |
| pagehide | ページ離脱時(破棄/凍結の両方) | true ならこれから凍結される |
| visibilitychange | 可視状態の変化時 | —(凍結直前の最終確実フック) |
復元時、load イベントは発火しません(ドキュメントは再ロードされていないため)。代わりに pageshow が event.persisted === true で発火します。したがって「ページ表示のたびに走らせたい初期化」は load だけでなく pageshow にも置く必要があります。逆に離脱時の後始末は、unload ではなく pagehide に書きます。
// 復元の検知:bfcache から戻ったときだけ true
window.addEventListener('pageshow', (event) => {
if (event.persisted) {
// 凍結中に止まっていた時計や、古くなったデータの再取得を行う
refreshTimestampClock();
revalidateStaleData();
}
});
// 離脱の検知:unload ではなく pagehide を使う(unload は bfcache を阻害する)
window.addEventListener('pagehide', (event) => {
if (event.persisted) {
// これから凍結される:接続を閉じ、復元時に張り直せる状態にする
closeWebSocket();
} else {
// 完全に破棄される:最後のデータ送信はここで(sendBeacon が安全)
navigator.sendBeacon('/log', collectMetrics());
}
});
pagehide の event.persisted を見れば、凍結されようとしているのか(true)/完全に破棄されるのか(false) を区別できます。凍結なら接続だけ閉じて状態は残し、破棄なら最終ログを送る、という分岐が書けます。離脱ログの送信には、凍結・破棄のどちらでも確実に飛ぶ navigator.sendBeacon() が適します。
Page Lifecycle API は freeze / resume イベントも提供します。freeze は凍結直前(pagehide の後)、resume は復元直後(pageshow の前)に発火し、より細かく「止める処理/再開する処理」を分離できます。たとえば freeze でアニメーションのループや高頻度ポーリングを止め、resume で再開すれば、凍結中に無駄な CPU を使わず、復元後に滑らかに動き出します。
凍結中の JS と復元後の状態整合
凍結されたページの JS は実行が一時停止します。setTimeout のコールバックは凍結時刻で止まり、復元されると残り時間から再開されるわけではなく、止まっていた分だけ「時間が飛んだ」ように見えます。タイマーやアニメーションの内部状態は イベントループ のタスクキューごと保留され、resume/pageshow のタイミングでまとめて処理が再開されます。
ここで生じる実務上の問題が state の陳腐化 です。凍結中に時間が経つと、表示中のデータが古くなります。たとえば「最終更新: ○分前」のような相対時刻、認証トークンの有効期限、株価やカートの在庫数などは、凍結前の値のまま復元されます。だからこそ pageshow で event.persisted を見て、復元時にだけ必要なデータを再取得・再計算するのが定石です。すべてを無条件に再取得すると bfcache の速度メリットを打ち消すため、「古くなって困るものだけ」を選んで更新します。
bfcache 復元では JS のグローバル変数も DOM もそのまま戻るため、「ログアウトしたのに戻るで操作画面が見える」「在庫切れ商品がカートに入ったまま見える」といった状態不整合が起こり得ます。これは bfcache の不具合ではなく、復元前提の設計が欠けているだけです。認証状態に依存する画面は pageshow(persisted)で必ずサーバー状態を再検証し、無効なら再描画・遷移する。「戻るで蘇る」ことを前提に、復元時の検証フックを必ず用意してください。
計測と検証
bfcache が効いているかは、ブラウザの開発者ツールで判定できます。Chrome では DevTools の Application パネルに「Back/forward cache」のテスト機能があり、実際に凍結を試みて非適格なら理由(どの要因で弾かれたか)を列挙してくれます。unload 由来か no-store 由来かが具体的に分かるため、原因の切り分けが速くなります。
bfcache はナビゲーションの体感を大きく左右するため、Core Web Vitals の文脈でも重要です。復元は新規ロードではないため、戻る操作で LCP 等が「速い側」に倒れます。逆に bfcache が効かないと戻るのたびにフルロードのコストが乗り、フィールドデータの数値を悪化させます。
頻出は次の4点です。❶ bfcache は HTTP キャッシュと違い「組み上がった生きたページ状態」をメモリに凍結し、戻る/進むで再パースなしに復元する。❷ 復元時は load ではなく pageshow(event.persisted === true) が発火する。❸ 阻害要因の代表は unload の登録・Cache-Control: no-store・開いた接続(WebSocket/IndexedDB 等)・許可ダイアログ表示中。❹ 離脱処理は unload ではなく pagehide に書き、ログ送信は sendBeacon を使う。unload が bfcache を無効化する理由を説明できるかが分岐点です。
まとめ
bfcache(Back/Forward Cache) は、遷移時にページを破棄せず DOM・JS ヒープ・実行状態ごとメモリに凍結し、戻る/進むで再パースもネットワークもなく即復元する仕組みです。これは取得バイト列を再利用する HTTP キャッシュ とは別物で、保存対象は「組み上がった後の生きたページ状態」です。凍結は適格性チェックを通った場合だけ働き、unload の登録・Cache-Control: no-store・開いた WebSocket や IndexedDB トランザクション・許可ダイアログ表示中などが阻害要因になります。復元時は load ではなく pageshow(event.persisted === true) が発火し、離脱処理は pagehide に書きます。凍結中は イベントループ のタスクが保留され JS が止まるため、復元時に陳腐化しうる認証状態・相対時刻・在庫などは pageshow で再検証する設計が必須です。これらを押さえれば、戻る/進むの体感速度を Core Web Vitals ごと最大化できます。
Web/フロントエンド Article
Back/Forward Cache(bfcache)の保存条件と復元を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
bfcache
比較で見る軸
難易度: advanced / カテゴリ: Web/フロントエンド / タグ数: 5
導入後に効く点
unload イベントの登録、Cache-Control: no-store、開いたままの WebSocket/IndexedDB トランザクション、許可ダイアログ表示中などは bfcache 入りを阻害する。pagehide の event.persisted で凍結されたか判定できる。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- Web/フロントエンド
- タグ数
- 5
判断チェックリスト
- 自社の用途が「bfcache / ブラウザ」に近いか確認する。
- 強みである「bfcache はページを破棄せず、DOM・JS ヒープ・実行状態ごとメモリに凍結する。戻る/進む時はネットワークも再パースもなくスナップショットを復元するため、表示は数十ミリ秒で完了する。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。