Stanby Tech Blog

求人検索エンジン「スタンバイ」を運営するスタンバイの開発組織やエンジニアリングについて発信するブログです。

Nuxtアップデートを機に見直したbfcache対応と、UX・分析の両立への取り組み

プロダクト部User Groupの藤澤です。
今回は、我々のプロダクト「スタンバイ」で, Nuxtアップデートをきっかけに顕在化した bfcache(Back/Forward Cache)による計測課題と、 それに対する対応・改善の取り組みについて共有します。 今回の改善により、これまでより正確な数値が取得できるようになり、社内指標の明確化が進みました。

この記事のポイント

  • Nuxtのマイナーアップデートをきっかけに、起きるはずのないPVの変動が発生
  • 原因は、bfcacheから復元した際のログ送信が不安定だったこと
  • UXを維持するため、bfcacheの無効化は採用せず、bfcacheからの復元時にもログを送信することで解決
  • UXと正確な分析を両立し、既存で拾えていなかった約15%の「本来あるべきPV」も取り戻せた

1. はじめに: Nuxtアップデートで露見した「PVのブレ」

スタンバイ」では、脆弱性対応のためにNuxt v3.12.2からv3.12.4へのマイナーアップデートを行いました。

しかし、アップデートした後である「異変」に気づきました。 Google Analytics (GA) の page_view や社内分析基盤のログとして計測しているPV(ページビュー)が、わずかに減少していたのです。

PVは我々のプロダクトにおける最重要ビジネス指標(KPI)の1つです。理由もなく変動している状態は好ましくありません。

調査の結果、Nuxtアップデートにより bfcacheから復元されるページの割合が変化したことが、PVのブレの主因であると判明しました。 これは、ブラウザ側の仕様であるbfcacheの動きをコード側でコントロール出来ていないことを意味します。


2. bfcacheとは? ブラウザの「高速復元機能」とPVのブレの関係

ここでbfcacheについて簡単に解説します。

2-1. bfcache(Back/Forward Cache)とは

bfcache(Back/Forward Cache)は、ブラウザがページ遷移時に直前のページ全体をメモリ上にまるごと保持しておき、ユーザーが「戻る」「進む」操作をしたときにその状態を即座に復元する仕組みです。

通常のキャッシュ(HTTPキャッシュ)は「静的リソース(HTML, JS, CSS, 画像など)」を保存するのに対し、bfcacheは JavaScriptの実行状態・DOMツリー・スクロール位置などページの状態 をそのままメモリに残します。 つまり、ユーザーが「戻る」ボタンを押した瞬間、ネットワーク通信なし・レンダリングなしで完全な状態を再現できるのです。

これにより、ユーザーは「戻る」ボタンを押した瞬間、読み込み時間ゼロで前のページに戻れて、ユーザー体験(UX)が劇的に向上します。

bfcacheの恩恵が分かる動画

bfcache: Back/Forward Cache

この動画では、bfcacheを使用した場合と未使用の場合の「戻る」操作の速度を比較しています。 bfcacheが有効な場合、ページ復元が瞬時に行われ、ユーザーは待ち時間なく前のページに戻れます。 一方、bfcacheが無効な場合は、通常の再読み込みが発生し、明らかに表示が遅くなります。

実際の挙動を動画で確認することで、bfcacheのUX向上効果を直感的に理解できます。

たとえば、検索結果一覧から求人詳細ページを開き、再び「戻る」操作をした際、bfcacheによって検索条件やスクロール位置が完全に保持された状態で即座に復帰します。 これにより、ユーザーは「検索をやり直す」「再描画を待つ」といった手間から解放されます。
(※この高速復元はブラウザ側の機能として実現されています)

参考資料: bfcache - web.dev (Google)

2-2. bfcacheのブラウザ依存性

bfcacheはブラウザ実装側の機能であり、ブラウザやバージョンによって挙動やサポート状況が異なります。
たとえば、Chrome・Firefox・Safariはいずれもbfcacheをサポートしていますが、復元のタイミングや保持条件(Service Workerやイベントリスナーの有無など)には微妙な差異があります。
つまり、「同じページでも、ブラウザによってbfcache復元が発生したりしなかったりする」ことがあるのです。

これが今回起きたPVのブレを生み出していました。

多くのモダンブラウザではbfcacheはデフォルトで有効化されており、開発者が特別に無効化しない限り、ユーザーの操作に応じて自動的に適用されます。

2-3. なぜPVログにブレが生じたのか?

問題は、bfcacheからの復元は、通常のページロードとは異なる点です。

通常のページロードでは、DOMContentLoadedload といったイベントが発火します。 VueやNuxtの場合、コンポーネントが再マウントされます。その際に onMounted フックが実行されます。

しかし、bfcacheから復元された場合、これらのイベントやライフサイクルフックは一切実行されません。ページはメモリから「解凍」されるだけです。

我々のプロダクトでは、GAや社内分析基盤のログのPVの多くを onMounted フック内で送信していました。 つまり、bfcacheから復元されると、onMounted は実行されず、ログ送信処理が丸ごとスキップされていたのです。

これは、Googleのweb.devでも、アナリティクス実装における一般的な落とし穴として言及されています。

bfcache は、分析の実装方法に影響を与える可能性があります。 ...bfcache からの復元は新しいページ読み込みとしてカウントされないため、onload イベントに依存している分析ライブラリでは、これらの「復元」が計測されません。

出典: bfcache - アナリティクスと閲覧の測定


改善前の実装例とその問題点

実際に、従来は以下のような実装でPVログを送信していました。

// 各ページの <script setup> 内
import { onMounted } from 'vue';

// 既存のログ送信関数(例)
const sendPageLog = () => {
  console.log('PVログを送信しました');
};

onMounted(() => {
  sendPageLog();
});

この実装では、bfcacheから復元された場合はonMountedが発火しないため、PVログが送れていませんでした。


3. bfcache問題への2つのアプローチ

この問題に対し、我々は2つの選択肢がありました。

選択肢A: Cache-Control: no-store でbfcacheを無効化する

サーバーのレスポンスヘッダーに Cache-Control: no-store を設定し、bfcacheをプロダクト全体で強制的に無効化する対応です。

  • メリット:
    • 常に通常のページロードが発生するため onMounted は必ず実行され、ログ未送信は即座に解消できる
  • デメリット:
    • bfcacheによる高速なUXの恩恵をユーザーから奪ってしまう
    • ブラウザバック・フォワード時に常にリロードが発生するため、PV以外の社内指標にも悪影響が出てしまう

選択肢B: bfcacheを有効のまま、復元時のログ未送信を個別に対策する

bfcacheは有効なまま(UXを維持)にし、bfcacheからの復元時にもログが送信されるよう、根本的な改修をする。

  • メリット:
    • 高速なUXと、正確なログ分析を両立できる
  • デメリット:
    • ログ送信はページごとに個別実装されている箇所が多く、影響範囲の調査と修正の工数が非常に大きい

我々は 高速UXを維持しつつ、従来欠損していたPVを正確に取得することを重視し、「選択肢B」を採用しました


4. 解決策:pageshow イベントで復元を検知する

「選択肢B」を実現する鍵が、pageshow イベントです。

pageshow イベントは、ページが表示されるたび(通常のロード時も、bfcacheからの復元時も)に発火します。 そして、イベントオブジェクトの event.persisted プロパティを見ることで、bfcacheから復元されたかどうかを判別できます。

  • event.persisted === true: bfcacheから復元された
  • event.persisted === false: 通常のページロード

この仕組みを使い、以下のように実装を修正しました。

// 各ページの <script setup> 内
import { onMounted } from 'vue';

// 既存のログ送信関数(例)
const sendPageLog = () => {
  console.log('PVログを送信しました');
};

onMounted(() => {
  // 1. 通常のページ遷移・リロード時のログ送信
  sendPageLog();

  // 2. bfcacheからの復元時にログを送信するためのリスナーを登録
  // NOTE: ブラウザバック・フォワード時にログを送信
  window.addEventListener("pageshow", (event) => {
    // bfcacheから復元された場合(persisted: true)のみ、ログを送信
    if (event.persisted) {
       sendPageLog();
    }
  });
});

この修正により、我々はbfcacheからの復元時にも確実にログを送信できるようになりました。


5. 導入プロセスと成果:ブレがあったPVに正確さを取り戻す

以前から我々のプロダクトでは、bfcacheは有効でした。 しかし復元時の対応をしていなかった為、ブラウザバックやフォワード操作でPVがブレやすい状況になっていました。

今回の対応により、従来計測できなかった復元時のPVも正確に取得可能になり、結果として全体PVが増えたように見えます。

そのため、以下の手順で慎重に導入を進めました。

Step 1. ブレの解消によって増加するPVの規模を把握(no-store テスト)

一度「選択肢A(Cache-Control: no-store)」を本番環境で短時間リリースしました。 これにより、bfcacheを無効化した場合にPVがどれだけ「増える」か(=これまで、どれだけブレによりPVを取りこぼしていたか)を計測しました。 結果、約15% のPVがbfcacheによって取りこぼしていたことを把握できました。

Step 2. 本対応(pageshow 対応)と関係者連携

次に、no-store の設定を元に戻し、本命の「選択肢B(pageshow 対応)」を行いました。 その際、事前にPdMやデータ分析チームに対し、「今回の対応でPVが約15%増加する。これはバグが修正され、正しい値が計測されるようになるためである」という周知を徹底しました。

Step 3. 成果

リリース後、PVは事前の試算通り約15%増加し、安定しました。 これは、Nuxtアップデートで顕在化したbfcacheによるログの取りこぼしを解消し、PVを「正しい状態」に引き直したことを意味します。

  • 分析面: ビジネスKPIであるPVを、bfcacheの挙動に左右されず、正確に計測できるようになった。
  • UX面: bfcacheは有効なままであるため、UXを一切損ねていない。むしろ、Nuxtアップデートによってbfcacheの復元率が上がったことで、プロダクト全体の「戻る」「進む」体験は以前より向上した。

6. まとめ:Nuxtアップデートが気づかせてくれたbfcache対応の重要性

今回の経験から我々が得た学びは以下の通りです。

  1. マイナーアップデートで「隠れた負債」を暴き出すことがある: ログの取りこぼしという問題は以前から存在していたが、Nuxtアップデートがbfcacheの挙動を変えたことで、その影響が無視できないレベルで顕在化した。

  2. 「正しい数字」を追うことの重要性: PVは重要なビジネス指標である。bfcacheによる意図しないPVのブレを防ぎ、正確な分析結果を取り戻すことができた。

また、安易な回避策(no-store)に頼らず、根本原因であるpageshow未対応に正面から取り組む事が出来ました。それによりUXと分析を両立でき、プロダクトの品質向上に大きく貢献できたと感じています。

この提案を承認して、リソース調整や実装面でサポートしてくれたチームやプロダクトオーナーにも恵まれていたなと、改めて感じました。

注意: bfcacheの挙動やpageshowイベントのサポート状況は、主要ブラウザ(Chrome, Firefox, Safari等)で異なる場合があります。特にSafariはbfcacheの仕様や復元タイミングが他ブラウザと異なることがあるため、実装時は各ブラウザの公式ドキュメントや挙動検証を推奨します。

スタンバイのプロダクトや組織について詳しく知りたい方は、気軽にご相談ください。 www.wantedly.com