設問と適用シナリオ
Eコマースのチェックアウトページが、履歴ナビゲーションによって異なる挙動を示します。「戻る」操作によって、正確なDOM、フォームの値、スクロール位置、JavaScriptヒープがほぼ瞬時に復元されることがあります。その復元されたスナップショットには、古いカートの合計金額や、別タブでサインアウトされたセッションが含まれている可能性があります。一方で、「戻る」操作が新しいドキュメントを生成し、通常の読み込みパスを実行することもあります。チームはこれらすべての結果を「ブラウザキャッシュ」と呼んでおり、グローバルにキャッシュを無効化することを検討しています。
back/forward cache(bfcache)がHTTPキャッシュやSPAルーターキャッシュとどのように異なるかを説明してください。その上で、以下の要件を満たすライフサイクルおよびデバッグ計画を設計してください。
- 通常のドキュメント読み込みと区別して、確認されたbfcache復元を検出する。
- 重要なアクションを許可する前に、認証、カート、その他の機密状態を再検証する。
- ページが非表示の間はリソースを解放し、復元後にそれらを正確に1回だけ再接続する。
- ページ内、子フレーム、サードパーティコードにおける適格性のブロッカーを特定する。
- ブラウザのバージョンごとに、履歴トラバーサル、確認された復元、ミス、およびブロック理由を測定する。
- プロダクトのセキュリティポリシーと互換性がある場合に、パフォーマンス上の利点を維持する。
この質問は、シニアフロントエンド、Webパフォーマンス、ブラウザプラットフォーム、フロントエンドアーキテクチャの面接に適用されます。単にキャッシュヘッダーを暗記しているかだけでなく、一時停止されたドキュメントについて論理的に思考できるかをテストします。
面接官が評価するポイント
第一に、候補者は3つの異なるメカニズムを明確に区別できなければなりません。HTTPキャッシュは再利用可能なレスポンスを保存し、リクエスト、鮮度、再検証のルールを適用します。bfcacheは、DOMやJavaScriptヒープを含むドキュメント全体のページをメモリ内に保持し、セッション履歴トラバーサルのために一時停止および再開することがあります。クライアントルーターは、ブラウザ管理のドキュメントを生成または復元することなく、ソフトナビゲーションのためにアプリケーションデータやUIを保持する場合があります。一方のキャッシュをクリアすることは、他方の診断にはなりません。
第二に、候補者はライフサイクルのエビデンスを理解している必要があります。pageshowは初期読み込み時にも発火するため、このイベント単体では証拠になりません。persisted値がtrueであるpageshowイベントによって、ドキュメントがbfcacheなどのキャッシュから復元されたことが確認されます。pagehideにおいて、persisted: trueはブラウザがページを保持する意図があることを意味しますが、ページがキャッシュされたまま残ることや、後に復元されることを保証するものではありません。
第三に、優れた回答では復元を「正確性の境界」として扱います。タイマーやメモリ内の値は以前の時点から再開されます。認可の責務は依然としてサーバーにあり、機密状態は確認された復元時に再検証されるべきです。アクティブな接続、データベースハンドル、オブザーバー、ロックは、ナビゲーション前に閉じるか切断し、復帰後に再度開く必要がある場合があります。再接続コードは冪等(idempotent)でなければなりません。
第四に、候補者は固定的なブロッカーリストに頼るのではなく、エビデンスに基づいてデバッグを行うべきです。適格性はブラウザ、バージョン、フレーム、API、レスポンスポリシー、メモリ圧迫、ナビゲーションシナリオによって変化します。Chrome DevToolsは隔離されたbfcacheテストを実行し、ブロッカーを分類できます。サポートされている環境では、PerformanceNavigationTiming.notRestoredReasonsがフィールドエビデンスとフレームツリーを提供しますが、その理由文字列は変更される可能性があり、クロスオリジンの詳細はマスクされることがあります。
最後に、候補者は測定可能な結果を定義する必要があります。back_forwardとして報告される履歴トラバーサルは、それ自体がbfcacheヒットの証拠ではありません。確認されたヒットはpageshow.persistedから得られます。ミスは履歴トラバーサル経由で到達した新しいドキュメントの読み込みであり、その原因は推測するのではなくセグメント化して分析する必要があります。目標は、古いセッションでのアクション、重複接続、歪んだアナリティクスを発生させずに、安全な復元カバレッジを高めることです。
回答前に明確にすべき質問
- どのナビゲーションが関係しているか? フルドキュメントナビゲーション、SPAのソフトルート変更、リロード、再オープンされたタブ、あるいは同一タブ内での実際の「戻る/進む」トラバーサルのいずれか?
- 何が安全に残存してよいか? フォームの下書きやスクロール位置は望ましい場合がありますが、認証、価格、在庫、権限、ワンタイムトークンには信頼できる鮮度ルールが必要です。
- レスポンスポリシーは何を要求しているか? 一部のページには、プロダクトのポリシーによりページスナップショットへの保持が禁止されているデータが含まれています。この要件はヒット率よりも優先されます。
- どのリソースがアクティブなまま残るか? 在庫監視のWebSocket、IndexedDB接続、Web Locks、メディアキャプチャ、オブザーバー、サードパーティスクリプトは、正確性や適格性に影響を与える可能性があります。
- どのブラウザとバージョンが対象か? DevToolsの出力や
notRestoredReasonsは移植性のある共通仕様ではありません。実際の履歴ナビゲーションを用いて、サポート対象ブラウザのマトリクスをテストしてください。 - 子フレームは存在するか? 同一オリジンの子フレームは詳細なブロッカーツリーを公開できます。クロスオリジンフレームはマスクされた情報のみを表示する場合があり、ベンダーの隔離や最小限の再現環境が必要になります。
- 「古い(stale)」とは何を意味するか? 許容可能な最大保持期間と、再検証に失敗している間にブロックすべきアクションを定義してください。
- アナリティクスはどのようにカウントされるか? 復元されたページがページビューなのか、ナビゲーションなのか、あるいはその両方なのかを決定し、1回の復元で重複したイベントが発行されないようにします。
- 成功の判定基準(ゲート)は何か? 観測可能な履歴トラバーサルにおける確認された復元の割合、ミス理由、復元レイテンシ、再検証失敗率、古いアクションの防止、および重複リソース数を追跡します。
30秒の回答フレームワーク
「私はbfcacheをHTTPキャッシュやルーターキャッシュと明確に区別します。bfcacheはブラウザ管理の履歴ナビゲーションのためにドキュメントを一時停止するものです。ヒットの確認はpageshow.persistedのみを用いて行い、復元を前提としない冪等なクリーンアップにはpagehideを使用します。復元時には、機密アクションの前にセッション、カート、価格、権限を再検証し、リソースを1回だけ再接続します。サポート対象ブラウザでミスを再現し、Chromeのbfcacheパネルと利用可能なnotRestoredReasonsデータを活用して、フィールド指標をブラウザとルートごとにセグメント化します。サーバー認可とスナップショット禁止ポリシーの権威性を維持しつつ、安全なページを最適化します。」
ステップごとの詳細解説
ステップ1:観測可能な4つのパスをモデル化する
ヘッダーではなく、実際の結果から整理します:
| パス | 新規ドキュメント? | 主なエビデンス | 必要な処理 |
|---|---|---|---|
| 初回またはリンクナビゲーション | はい | pageshow.persisted === false; 通常ナビゲーションタイプはnavigate | 状態とリソースの初期化 |
| リロード | はい | pageshow.persisted === false; ナビゲーションタイプはreload | 通常の読み込みパスを実行 |
| リロードを伴う履歴トラバーサル | はい | pageshow.persisted === false; ナビゲーションタイプはback_forward | bfcacheミスを記録し理由を調査 |
| 確認されたbfcache復元 | いいえ | pageshow.persisted === true | 機密状態を再検証し安全に再開 |
ナビゲーションタイプは、新しいドキュメントにどのように到達したかを示します。ヒットした場合は古いドキュメントが再開されるため、ナビゲーションタイプ自体でbfcacheヒットを確認することはできません。ただし、履歴トラバーサルによって引き起こされた新しい読み込みを特定できるため、ミスの分母のカウントに寄与します。ブラウザの再起動、タブの複製、再オープンフローによってこのシグナルが曖昧になることがあるため、フィールド指標には利用可能な場合、ブラウザ、バージョン、ルート、シナリオを含める必要があります。
また、bfcacheはHTTP再検証とも異なります。復元時、ブラウザはメモリ内のページを再開します。Cache-Control: no-cacheがあっても、ピクセルが表示される前にネットワーク検証が強制されるわけではありません。復元されたコンテンツが最新である必要がある場合、アプリケーションはpageshowの後にターゲットを絞ったチェックを実行します。SPAのソフトナビゲーションは同一ドキュメント内で履歴とUIを変更しますが、bfcacheはそのドキュメント自体から離脱し、後に復元された際に関係してきます。
ステップ2:ライフサイクル処理を冪等かつ復元安全にする
接続の管理を一元化します。クリーンアップの後には、新規ドキュメントの読み込み、復元、あるいは全く復帰しないケースが続く可能性があります。セットアップは初回読み込み後だけでなく複数回の復元後にも実行される可能性があるため、どちらの操作も反復実行に耐えられる必要があります。
let liveSocket = null;
function connectLiveUpdates() {
if (liveSocket) return;
liveSocket = new WebSocket("wss://shop.example/live-cart");
}
function disconnectLiveUpdates() {
liveSocket?.close();
liveSocket = null;
}
async function revalidateSensitiveState() {
const response = await fetch("/api/session-snapshot", {
cache: "no-store",
credentials: "same-origin",
});
if (response.status === 401) {
location.replace("/login");
return false;
}
if (!response.ok) {
showRetryState();
return false;
}
renderSessionState(await response.json());
return true;
}
window.addEventListener("pagehide", disconnectLiveUpdates);
window.addEventListener("pageshow", async (event) => {
reportNavigation(event.persisted ? "bfcache_restore" : "document_load");
if (event.persisted && !(await revalidateSensitiveState())) return;
connectLiveUpdates();
});サンプルのサーバーエンドポイントが引き続き信頼の基点(権威)となります。復元された機密状態がチェックされている間、UIはチェックアウトを無効化すべきであり、チェックに失敗した場合はスナップショットを無条件に信頼するのではなく再試行状態を表示する必要があります。サーバー側のチェックアウトリクエストでは、引き続きユーザーの再認可を行い、価格と在庫を再計算しなければなりません。
重要なクリーンアップにunloadを使用しないでください。信頼性が低く、一部のブラウザではページがキャッシュ不適格になる原因となります。ページのライフサイクルのクリーンアップにはpagehideを優先し、ナビゲーションではなく視認性が要件である場合はvisibilitychangeを使用します。Chromium専用のfreezeやresumeのみに依存した正確性の設計は避けてください。
ステップ3:ローカル再現から担当箇所の特定までミスを診断する
確定的な手順で1つのルートを再現します。直接ページを開き、関連する状態を確立し、通常のリンクから2つ目のドキュメントへ遷移した後、ブラウザの「戻る」ボタンを使用します。コントロール実行(基準テスト)では拡張機能や開発ツールの影響でライフサイクルの挙動が変わることがあるため、これらを無効化してください。本番のレスポンスヘッダーやサードパーティスクリプトを含めて再現を繰り返します。
Chromeでは、ApplicationパネルのBack/forward cacheテストを実行します。結果がアクション可能(actionable)、ブラウザサポート待ち(pending browser support)、アクション不能(not actionable)のいずれであるかを記録し、フレームのアトリビューションを展開します。unloadリスナーが表示された場合、自社コードと外部ベンダーコードのどちらが登録したかを特定します。レスポンスポリシー、開いたままの接続、アクティブなトランザクション、ロック、メディアAPI、フレームが原因として挙がった場合、その結果を再現する最小限の機能まで絞り込みます。
サポートされている環境では、ミスとなった履歴トラバーサルによって到達した新規ドキュメントでnotRestoredReasonsを検査します。返された階層と理由の値をアプリケーションロジックとしてではなく、診断データとして保持します。完全一致の理由文字列をハードコードしたり、すべてのブラウザがプロパティを公開していると想定したり、null単体を確認されたヒットと解釈したりしないでください。クロスオリジンの子フレームの詳細はプライバシー保護のためマスクされることがあります。埋め込まれたベンダーを個別にテストするか、制御された実験で削除して因果関係を確立してください。
一度に1つの原因を修正し、隔離テストを再実行してから、ルート全体を検証します。pagehideでのIndexedDB接続やWebSocketの切断、unloadハンドラーの削除、サードパーティフレームの制約などは仮説の例であり、その変更が実際に適格性に影響するかどうかはブラウザのテスト結果によって判断されます。
ステップ4:現場環境(フィールド)での正確性とパフォーマンスを検証する
pageshowごとに1つのイベントを発行します。persisted: trueは確認された復元です。新規ドキュメントの読み込みについては、Navigation Timingタイプを付与します。back_forwardはbfcacheをミスした観測可能な履歴トラバーサルを特定します。これらのイベントは相互排他的に保ちます。プライベートなページコンテンツを記録することなく、ルートファミリー、ブラウザ、バージョン、認証状態、実験コホートを追加します。
最低限以下を測定します:
- 観測可能な履歴トラバーサルに対する確認された復元の割合(ブラウザおよびルート別でセグメント化)。
- ミス数およびサポートされているブロック理由ファミリー(マスクされたケースや利用不可のケースを含む)。
- 復元から次の描画まで(restore-to-next-paint)、および復元から機密状態が操作可能になるまでのレイテンシ。
- セッション、権限、カート、価格、在庫の再検証失敗率。
- 復元された状態が未検証の間にブロックされたチェックアウト試行回数。
- 「戻る/進む」サイクルの反復後におけるソケット、オブザーバー、アナリティクス、タイマーの重複副作用。
- メモリの回帰およびユーザーに見える不具合(ヒット率の最大化だけが唯一の目的ではないため)。
履歴操作の反復実行、別タブでのサインアウト、他画面でのカート変更、ワンタイムトークンの失効、再検証エンドポイントの障害発生、クロスオリジンフレームの動作確認などを実施します。対象範囲内のChrome、Firefox、Safariの各バージョンをテストします。ローカルのChromeパネルは制御された1つのケースを証明し、本番テレメトリは分布を証明し、プロダクトのアサーションは安全性を証明します。
質の高い模範解答
「瞬時に戻るパスはbfcacheによるものです。ブラウザがチェックアウトドキュメント全体とそのJavaScriptヒープを保持し、一時停止させ、セッション履歴ナビゲーション中に再開しました。HTTPキャッシュはレスポンスのみを保存し、クライアントルーターはアクティブなドキュメント内でのソフトナビゲーションを処理します。私ならまず各パスを識別します。pageshow.persisted === trueは復元を確認するものです。Navigation Timingタイプがback_forwardである新規ドキュメントは、このページがbfcacheから復元されなかった履歴トラバーサルを表します。pagehide.persisted === trueはブラウザがキャッシュしようとする意図に過ぎないため、ヒットの記録としては使用しません。」
「pagehideでは、冪等なクリーンアップを用いて、カートのソケットや開いているトランザクションなど、維持すべきでないリソースを閉じます。毎回のpageshowでは、リソースが正確に1回だけ接続されるようにします。復元が確認された場合は一時的にチェックアウトをブロックし、信頼できるセッションとカートのスナップショットを取得してUIを更新し、ログアウト状態であればリダイレクトします。チェックアウトAPIは引き続き認可、最新の価格、在庫をチェックするため、再開されたヒープ単体で勝手に購入が承認されることはありません。アナリティクスについては、初回読み込みのエフェクトをすべて再実行するのではなく、復元として1回カウントします。」
「ミスについては、本番ヘッダーとベンダーを含む正確なフルドキュメントパスを再現します。ChromeではApplicationパネルのbfcacheテストを実行し、フレームの所有権を展開して、アクション可能な原因を1つずつ修正します。サポートされているChromiumバージョンでは、ミスとなった履歴読み込みに対してnotRestoredReasonsを収集し、マスクされたケースや不明なケースもそのまま保持し、理由テキストに基づいてプロダクトの挙動を分岐させることは絶対に避けます。その後、実装やシナリオによって適格性が異なるため、サポート対象のChrome、Firefox、Safariのマトリクスをテストします。」
「リリースの判定基準は、確認された復元率の向上と復帰レイテンシの低減であり、テストにおいて古いセッションでのアクション、重複接続、アナリティクスの重複がゼロのままであることです。ミスと再検証の失敗をルートおよびブラウザごとにセグメント化します。特定のプライベートデータを含むページをスナップショットとして保持してはならないというポリシーがある場合は、その制限を維持し、ヒット率のためにセキュリティを犠牲にするのではなく周辺ページの最適化に注力します。」
よくある間違いと改善策
- すべての復帰パスを「ブラウザキャッシュ」と呼ぶ → HTTPレスポンス、ドキュメント全体のスナップショット、ルーター状態は異なるルールに従う → メカニズムを正確に命名し、メカニズム固有のエビデンスを収集する。
pageshow単体を証拠として使用する → 初回ドキュメント読み込み時にも発火する → 確認された復元にはevent.persisted === trueを必須とする。pagehide.persistedを将来のヒットとみなす → ブラウザが後からページを破棄したり、別のパスを選択したりする可能性がある → 安全なクリーンアップの指針としてのみ使用し、ヒットは復元時に記録する。back_forwardがbfcacheを意味すると仮定する → 履歴トラバーサルによって読み込まれた新規ドキュメントを表す場合がある → ミスのためのナビゲーションタイプと、ヒットのためのpageshow.persistedを組み合わせる。- 古いUIを修正するためにすべてのキャッシュを無効化する → ライフサイクルのバグを隠蔽し、瞬時ナビゲーションの恩恵を失う → 機密フィールドを再検証し、サーバー認可の権威性を維持する。
- 復元されたページをすべてリロードする → 一律のリロードは最適化を放棄することになり、障害時にループする可能性がある → 信頼できる状態のみを更新し、リロードやリダイレクトは定義された無効状態のために予約する。
- 固定のブロッカーリストを丸暗記する → 適格性や理由の名称はエンジンやバージョン間で変化する → ブラウザテスト、機能検出、フィールドエビデンスを活用する。
- クリーンアップを
unloadに配置する → イベントの信頼性が低く、適格性を阻害する可能性がある →pagehideを使用し、プロダクトの要件が視認性である場合はvisibilityイベントを使用する。 - ライフサイクルイベントごとに再接続する →
pageshow、resume、フレームワークのエフェクトによりソケットやオブザーバーが重複する可能性がある → 各リソースに1つの冪等な管理者(owner)を割り当てる。 - DevToolsの1回のテスト結果のみを過信する → 実際のベンダー、メモリ圧迫、ブラウザ、セッション変更を網羅できない → 制御されたテスト、ブラウザマトリクス、RUM、敵対的なプロダクトフローを組み合わせる。
関連する質問
Cache-Control: no-cacheはbfcache復元前の検証を強制しますか?
いいえ。bfcache復元は、HTTPキャッシュを介してリソースリクエストを処理するのではなく、メモリ内のドキュメントを再開するため、復元されたページが表示される前にHTTP再検証ディレクティブが実行されることはありません。pageshow.persistedを使用して、ターゲットを絞った鮮度チェックをトリガーしてください。ポリシーによってページスナップショットの保持自体が禁止されている場合は、適切なレスポンスおよびブラウザポリシーを適用し、適格性が実装によって異なる場合があることを受け入れます。
PerformanceNavigationTiming.type === "back_forward"はヒットのシグナルですか?
いいえ。これは新しく作成されたドキュメントに対し、履歴トラバーサル経由で到達したことを伝えているだけです。確認されたbfcache復元は既存のドキュメントを再開するものであり、pageshow.persisted === trueで識別されます。新規のback_forward読み込みは、ブラウザの再起動やタブの再オープンといった特殊な挙動を考慮した上で、観測可能なミスとしてカウントし、適切にテレメトリをセグメント化してください。
notRestoredReasonsが存在しない、nullである、またはマスクされている場合はどうなりますか?
存在しない場合は非サポートとして扱い、nullは単体では不十分とみなし、マスクされている場合はプライバシーを保護した診断カテゴリとして扱います。ヒットの確認にはページ遷移イベントを使用します。ミスについてはローカルで再現し、利用可能なDevTools出力を調査し、子フレームやサードパーティスクリプトを隔離して、原因を捏造するのではなく「不明(unknown)」バケットを維持してください。
アナリティクスは復元されたページをどのように処理すべきですか?
まずプロダクトとしての定義を決定します。復元をページビューとしてカウントする場合は、pageshow.persistedから1つのイベントを発行し、初回読み込み時の計測ロジックを再実行しないようにします。アナリストがドキュメント読み込みと復元を分離できるように、ナビゲーション種別フィールドを含めます。必要に応じて訪問スコープのパフォーマンス累積値をリセットし、「戻る/進む」の反復サイクルで重複が発生しないかテストしてください。
認証が必要なチェックアウトページでbfcacheを使用すべきですか?
セキュリティポリシーがページスナップショットを許可しており、復元パスを安全に構築できる場合に限られます。復元時にはセッションと重要なデータを再検証し、チェックが完了するまで機密アクションをブロックし、サーバー側での認可と価格検証を必須として維持します。プライベートなコンテンツを履歴から復元可能な状態で残してはならない場合は、そのポリシーを適用し、bfcacheの最適化はより機密性の低いルートに集中させてください。