🐓

ブラウザバックをトリガーに処理をしようとしたらコケた話

2024/12/01に公開

BABY JOB株式会社の横田と申します。
去年の12月よりBABY JOB株式会社にジョインして日々楽しくエンジョイしながら働いております。
1年間業務を続けていると、色んなことでコケることも多く、まさにコケまくりの日々です 🐓

今回はそんな「コケ話」のひとつ「ブラウザバックをトリガーに処理をしようとしたらコケた話」をお届けします🎄

ブラウザバックを検知したい状況がある

老若男女問わずそういう状況、あると思います。え?ない?
まぁ、ありますよね!そういうことで話を進めます。

ブラウザバックを検知したい状況は大きく分けて以下の2種類があると思います。

  1. 遷移前にブラウザバックによるイベントが発生したことを検知したい
    例→フォーム入力中にページを離れようとしたときの警告など
  2. 遷移後にブラウザバックでナビゲートされたかを検知したい
    例→戻る操作で前の画面に戻った際に、内容を更新したい

今回、私がコケたのは「2.」の状況です。
「1.」については、ユーザーのブラウザ操作をJavaScriptで直接検知することは、プライバシーやセキュリティの観点から不可能です。
ブラウザ操作(バック・フォワード・リロード)か、そうでないか程度であれば検知する仕組みを実装できますが、今回は割愛します。

なぜ、検知したいと思い至ったか

業務で以下のようなユーザーフローを考える状況がありました。

フローの概要

  1. QRコードを読み取り、弊社サービスの決済ページにアクセス
  2. 決済ページで決済に必要な金額や名前などの情報を入力
  3. 外部の決済代行サービスへリンクで遷移
  4. 決済代行サービスで支払い手続きを行い、内容を確認する
  5. 完了画面が表示される

このとき、社内で以下の指摘が入りました:
「決済代行サービスから、ブラウザバックで決済ページに戻ってきた場合にどうなるのか?」

どうなるの?

  • 決済代行サービスに遷移後すぐのブラウザバックして決済ページに戻る
    → ⭕️ 問題なし
  • 支払い完了後にブラウザバックして決済ページに戻る
    → ❌ 再び「お支払いへ進む」ボタンが表示され、再度決済可能な状況に


二重支払いが可能なケースがある!

実際に、支払い完了画面から数回ブラウザバックして決済ページに戻ってくるユーザーがどれだけいるかは分かりません。
ただし、ユースケースとして再現可能である以上、未然に防ぎたいと考えました。

この話をしていたときのは横田は
🤔「あー、ブラウザバックを検知する方法あったよな…」
😊「戻ってきたときに内容をリセットすればいいか!」
と考えたのです。

ナレーション
<この考えが後に盛大にコケる原因になるとは、このときの横田は露にも思わなかった…>

ブラウザバックの検知方法

PerformanceNavigationTimingインターフェイスを使います。

PerformanceNavigationTimingとは?

パフォーマンスAPIの1つで、これ自体はブラウザバックを検知する専用のAPIではなく、ドキュメントのナビゲーションパフォーマンスを計測するインターフェイスです。
どのようにドキュメントにナビゲートしたのかを表す、読取専用のtypeプロパティというのを持っており、このプロパティを参照することでback_forward, reload, navigateなどナビゲートの種別を判別することが出来ます。

パフォーマンスAPIはwindow.performanceに生えているので、getEntriesByType()メソッドを使うことでPerformanceNavigationTimingにアクセスすることができます。

const performanceNavigationTiming = performance.getEntriesByType("navigation")[0]
// ナビゲーションは通常1ドキュメントに1つしか存在しないので、[0]を取る。

動作確認

これで、ブラウザバックorフォワードを判断できるようになりました。
動作を確認してみます。

  • Title: ページタイトル
  • Type: performanceNavigationTiming.typeの値
  • goto: 下層ページと別オリジンへのリンク

を画面に表示してChromeで確認すると…


Chromeで確認

いいですね!
同じオリジン内のページから戻ってきても、別オリジンのページから戻ってきてもback_fowardと認識されています。

Safariでも見てみます。


Safariで確認

このように back_fo...

re....load...?

リロード?

なぜ?ホワイ?

なぜSafariはブラウザバック時にreloadを返すのか

わからない…

色々調べてみましたが、おそらくこういう理由だろう程度の情報しかなく、なぜリロードするのかという理由が書かれた一次情報や、それに準ずる信頼できる情報を見つけることはできませんでした。

ただ、いくつかの情報と実際の挙動により、どういう状況の時にリロードするのかはわかりました。それは…

bfcacheが無効のとき

bfcacheとは?

bfcache(Back/forward cache)とは、ブラウザのレンダリングエンジンによって実装されている機能で、ユーザーがページを離脱する際に、そのページの状態を保持しておき、ブラウザバックやフォワードで戻ってきたときに、状態を復元して表示する仕組みです。
そのため、名前にcacheと付いていますが、性質的にはスナップショットであり、bfcacheが有効であれば、onloadイベントなどが発生しません。

bfcacheをONOFFする直接的なフラグは提供されておらず(※1)、bfcacheは条件が満たされていれば有効になります。

※ bfcacheの詳細な話はLINEヤフーさんとweb.devの以下の記事が大変参考になります。(※2)
https://techblog.yahoo.co.jp/entry/2022010530253635/

https://web.dev/articles/bfcache?hl=ja#observe-when-a-page-is-restored-from-bfcache

  • ※1 ブラウザの設定としては提供されています。例えばchrome://flagsBack-forward cacheと検索するとbfcacheの振る舞いを変更する項目が現れます。
  • ※2 最新のChroniumのコードは以下でも確認出来ます。Chronium Code Search

なぜ動作確認でbfcacheが無効になっていたのか

bfcacheが無効になる条件は複数あります。
先の動作確認ではViteの開発サーバで動作しており、WebSocketによる通信が行われていました。
WebSocketによる通信が行われているというのもbfcacheが無効になる条件になります。

Chromeの場合、bfcacheがなぜ無効になってるかをデベロッパーツールで確認することができます。

JSではpageshowpagehideのイベントオブジェクトが持つpersisted: booleanプロパティを参照することで、bfcacheの状態を確認することが可能です。

window.addEventListener('pageshow', (event) => {
  if (event.persisted) {
    console.log('bfcacheから復元された')
  }
})

window.addEventListener('pagehide', (event) => {
  if (event.persisted) {
    console.log('bfcacheに保存された…かも')
    // 保存する意思はあるが、何かしらの理由により保存できないこともあります。
  }
})

先の動作確認のコードに上記pageshowイベントハンドラを設定してbfcacheが有効になっているかを確認してみます。


WebSocketに通信よる通信のためbfcacheは無効

Use BFCachenoになっており、bfcacheが無効になっていることが確認できました。

bfcacheを有効にしてみる

WebSocketによる通信が行われていることが無効化される原因だったので、Viteのプレビューモードで確認してみます。

確認できました。Viteのプレビューモードは静的ファイルを読んでいるだけなのでWebSocketによる通信が発生しません。
persistedプロパティの値も見てみます。

Use BFCacheyesになっており、bfcacheが有効になっていることが確認できました。
また、スナップショットが復元されたため、PerformanceNavigationTiming.typeの値は期待しているback_forwardではなく、遷移前の値navigateのままになっています。

bfcacheのまとめ

  • bfcacheはレンダリングエンジンによって実装されている機能である
  • bfcacheは条件がそろえば自動で有効化される
  • bfcacheから復元された場合、ナビゲーションに関わるイベントは発生しない
  • 無効の場合の挙動はブラウザによって違う

改めて、どのように検知するのか

bfcacheが有効な環境ではブラウザバックをしてもナビゲーションが発生しないため、PerformanceNavigationTiming.typeの値は復元された値になりブラウザバックを検知する値としては使えなそうです。

しかし、よくよく考えみるとbfcacheというのはスナップショットの保存と復元をする機能です。復元できるということは保存したページに戻ってきたということで、つまり…

window.addEventListener('pageshow', (event) => {
  if (event.persisted) {
    // これで良いのでは?
    type = 'back_foward_cache'
  }
})


Chromeで確認

いいですね。Safariでも確認してみます。


Safariで確認

Type の値が期待した通りの値になり、ブラウザ間の差異もなくなりました🙌

bfcacheが無効のときはSafariがリロードするという問題は依然残っているので、bfcacheが有効であることが前提ですが、使えそうです。

※ 同様の方法がweb-vitalsでも実装されていました。

問題解決!いざ!

この方法は使わないことにしました。
「な、なんだってー!?」という声が聞こえてきそうですが、使わないことにしました。

理由

Safariでは、bfcacheが無効になるとページがリロードされる問題があります。
(実際リロードされてる)
さらに、bfcacheが無効になる条件は複数存在し、不透明・不確定な部分もあります。
こうした不透明・不確定要素を決済の手続きが抱えたままでは、サービスの品質やユーザー体験に影響を与えるだけでなく、開発者にとっても大きな負担となります。

さらに、そもそもの対応方法自体が適切ではなかったと感じています。
「い、いまさらー!?」という声が聞こえてきそうですが、改めて振り返ると、対応の方向性を見誤っていました。

今回の問題の本質は、決済の手続きという一貫性を保つべき場面で、ブラウザバックによりやり直しが可能になってしまう点にあります。
ブラウザバックはあくまで「やり直し」の手段の一つに過ぎず、これをトリガーとして処理を制御するのは適切ではない、いわば「悪手」だと考えています。

どのように問題に対処したのか

ブラウザバックをトリガーに処理を行うのではなく、別ページへの遷移をトリガーに処理を行う方法に変更しました。
これにより、ブラウザバックで戻ってきた際にbfcacheで復元されようがされまいが、再度決済を行うことはできなくなります。

ただし、この方法にも課題があります。たとえば、ユーザーが間違って遷移してすぐ戻った場合も処理済みとみなされ、再決済ができなくなります。この場合、金額や入力内容を再入力する必要があるため、UXの観点では改善の余地があります。

それでも、二重決済のようなクリティカルな問題は防止できるため、この方法を一旦採用し、カイゼンサイクルの中で改修を進める予定です。
※ 遷移前に「やり直しはできません」的なメッセージを表示するだけでも効果があるかもしれませんね。

今回の気付き

  • ナビゲーションの種別を判別する際は、bfcacheの影響を考慮する必要がある
  • 問題を解決する際には本質に立ち返ること

いやー今回も良いコケっぷりでした 🐓<コケーッ!
この失敗が、あなたの開発に少しでも役立つヒントになれば嬉しいです✨

それではシーユーアゲイン!またコケる日まで!! (すぐやん…


  • QRコードは株式会社デンソーウェーブの登録商標です
BABY JOB  テックブログ

Discussion