ユーザーはブラウザの他のほぼどのコントロールよりも頻繁に「戻る」ボタンを押します。彼らは、前の画面が、自分が離れた場所と全く同じ状態で即座に表示されることを期待しています。現代のブラウザは、バック/フォワードキャッシュ(bfcache)によってその期待に応えています。ページから離脱したときにページを破棄する代わりに、ブラウザはメモリ内でページを凍結(フリーズ)させます。戻ったときには、そのスナップショットを復元します。ブラウザはHTMLのパース、JavaScriptの再実行、レイアウトの再計算をスキップします。ページが完全に消滅したわけではないため、結果として瞬時に感じられるのです。

bfcacheが実際に行っていること

通常のページロードはコストがかかります。ブラウザはリソースを取得し、HTMLをトークン化し、DOMを構築し、スクリプトを実行し、スタイルを解決し、レイアウトを実行し、ピクセルを描画し、レイヤーを合成しなければなりません。bfcacheは、ページをRAM内で凍結した状態で維持することで、そのほとんどを回避します。これはディスクキャッシュではありません。レンダリングされたページ(JavaScriptのヒープ、スクロール位置、フォームの状態を含む)は、ユーザーが次のページを読んでいる間、メモリ上に保持されます。ユーザーが「戻る」をクリックすると、ブラウザはスナップショットを解凍し、pageshow イベントを発火させます。ネットワークに触れたり、最初からレイアウトをやり直したりすることなく、ページが再開されます。低スペックのデバイスや不安定な接続を使用しているユーザーにとって、bfcacheによる復元と新規ロードの差は、数百ミリ秒以上に及ぶことがあります。

何がbfcacheを阻害するか

最近、ある開発者が、何が正確にbfcacheをブロックしているのかを突き止めるために、クリーンな実験を行いました。彼らは6つのシンプルなページを作成し、それぞれが疑わしいブロッカーを1つずつテストするようにし、その後ページを離れて「戻る」ボタンを押しました。結果は明白でした。

特殊なヘッダーやスクリプトを持たないベースラインのページは、正常に復元されました。beforeunload リスナーを持つページも、問題なく復元されました。驚くべきことに、Cache-Control: no-store で配信されたページもbfcacheに入り、以前のガイダンスと矛盾する結果となりました。凍結するには動的すぎると考えられがちな、ライブブログの記事でさえ、正常に復元されました。

2つのページが失敗しました。unload イベントリスナーを持つページは復元できませんでした。また、開いているWebSocket接続があるページもブロックされました。これら2つの失敗は、実際のプロダクションサイトが日々陥っている罠を指し示しています。

unloadイベントの罠

unload イベントは、長らく最後の瞬間のクリーンアップのための合図として使われてきました。開発者は、アナリティクスのビーコンを送信したり、タイマーを停止したり、一時的な状態を消去したりするためにこれを使用します。問題は、bfcacheが「ページが再び動き出す可能性がある」という考えに基づいていることです。ブラウザが unload リスナーを見つけると、ページが完全な破棄を想定していると判断し、凍結を拒否します。アタッチされた関数が空であっても関係ありません。リスナーが存在するということ自体が、すべての現代的なブラウザにおいてキャッシュを拒否するのに十分なのです。

代わりの手段は pagehide です。このイベントは、ページがbfcacheのために凍結されるときと、ページが本当に破棄されるときの両方で発火します。この2つを区別する必要がある場合は、ページがbfcacheに向かっているとき、event.persisted プロパティが true になります。ただし、ほとんどの終了処理タスクについては、pagehide が両方のパスをカバーします。すべてのクリーンアップロジックを unload から pagehide に移動してください。そして、サードパーティのアナリティクススニペットやレガシーなプラグインに隠れているものも含め、すべての unload リスナーを完全に削除してください。

アクティブな接続の罠

開いているネットワークまたはストレージの接続は、ページがまだ実際の作業を行っていることを示します。ブラウザはナビゲーションの瞬間にアクティブなリソースをインベントリ(目録)化します。開いているWebSocket、アクティブなWebRTCピア接続、または残っているIndexedDB接続が見つかると、ブラウザは凍結を中止し、ページを通常通り破棄します。バイトがまだ流れている可能性がある間は、スナップショットを信頼できないからです。

これらのリソースは pagehide リスナー内で閉じるべきです。WebSocketの close メソッドを呼び出してください。WebRTCのピア接続をシャットダウンしてください。未完了のIndexedDBトランザクションを中断(abort)またはコミットしてください。ユーザーが戻ったときにアプリがそれらのチャネルを必要とする場合は、pageshow 内で再開してください。この「pagehideで閉じ、pageshowで復元する」パターンにより、機能を失うことなく、ページを即時のバックナビゲーションの対象に保つことができます。

no-storeの驚き

長年、従来の常識では Cache-Control: no-store がbfcacheを妨げるとされてきました。Chromeは2025年にその挙動を変更しました。no-store で配信されたページも、現在はbfcacheに入ることができます。ブラウザは、認証状態やクッキーが保存された状態を無効にするような形で変化した場合にのみ、後で凍結されたスナップショットを破棄します。もしあなたが no-store を...として使用してきたのであれば、