JavaScriptでブラウザバックを検知して、画面の状態を切り替えたり、データを再取得したりしたいものの、「popstateイベントだけで本当に判定できるの?」「リロードや進む操作とどう区別すればいい?」と悩んでいませんか。
ブラウザバックの検知にはpopstateイベントがよく使われますが、BFCache(Back/Forward Cache)やブラウザごとの挙動まで考慮すると、単純にイベントを監視するだけでは対応しきれないケースがあります。特にフォームの入力内容を守ったり、決済処理の二重実行を防いだり、モーダルやドロワーの状態を履歴と連動させたりする場合は、ブラウザの履歴APIやページライフサイクルを正しく理解しておくことが重要です。
この記事では、JavaScriptでブラウザバックを検知する基本的な方法から、実際のWeb制作で役立つ実装方法、検知できない場合の原因やBFCacheへの対策まで、具体的なコード例を交えて解説します。
WEBCOACH|副業・フリーランス特化型のオンラインWebデザインスクールJavaScriptでブラウザバックを検知する基本方法
ユーザーがブラウザの「戻る」ボタンを押した際、画面の遷移を抑止したり、特定の処理(モーダルを閉じる、最新データを再取得するなど)を実行したいケースは多々存在します。しかし、JavaScriptには単体で「ブラウザバックの発生だけをピンポイントで購読する直感的な単一のイベント」は存在しません。
ブラウザバックを正確に検知・制御するには、ブラウザの履歴スタック(History Stack)の仕組みを理解した上で、目的に応じた適切なAPIを選択する必要があります。
ブラウザの履歴スタックとpopstateの仕組み
ブラウザはユーザーのページ遷移を「スタック構造(LIFO: Last In, First Out)」で管理しています。通常のページ遷移では履歴が積み上がりますが、
history.pushState()を使用すると、ページをリロードせずに履歴スタックへ新しい「ダミーの履歴エントリー」を追加できます。この状態でユーザーが「戻る」ボタンを押すと、スタックのトップが取り除かれ、1つ前の履歴に戻ります。この履歴スタックの変化(アクティブな履歴の変更)を検知して発火するのが
popstateイベントです。

popstateイベントとhistory.pushState()を組み合わせた確実な検知方法
popstateイベントは、ユーザーが「戻る」または「進む」ボタンをクリックして履歴間を移動した際にwindowオブジェクト上で発火します。ただし、単にpopstateのイベントリスナーを登録しただけでは、現在のページに留まったまま戻る挙動を検知・制御することはできません。
確実にブラウザバックをフックするには、「ページ読み込み時にhistory.pushState()でダミーの履歴を1つ追加し、ユーザーが戻るボタンを押した瞬間にpopstateを発生させる」というアプローチが標準的かつ確実な手法です。
// ページの初期化処理
document.addEventListener('DOMContentLoaded', () => {
// 1. 現在のページにダミーの履歴エントリーを追加する
// 第1引数: stateオブジェクト, 第2引数: title(基本空文字列), 第3引数: URL(省略可)
history.pushState({ page: 'detected' }, '', location.href);
// 2. popstateイベントリスナーを登録
window.addEventListener('popstate', (event) => {
// 戻るボタンが押された際の検知処理
console.log('ブラウザバックを検知しました。', event.state);
// 必要に応じて確認ダイアログ表示や状態変更を行う
const leavePage = confirm('入力途中のデータがあります。ページを離れますか?');
if (!leavePage) {
// 離脱を拒否する場合:再度pushStateを発行して履歴を押し戻し、ページを維持する
history.pushState({ page: 'detected' }, '', location.href);
} else {
// 離脱を許可する場合:ブラウザの本来の戻る挙動を実行させる
history.back();
}
});
});
実装時の必須ルールと注意点
- ユーザー操作の要件: モダンブラウザのセキュリティ制限により、ユーザーがページ上で何らかの操作(クリックやスクロールなど)を行わないまま
pushStateを発行しても、意図通りにpopstateが動作しない場合があります。 event.stateの活用:pushStateの第1引数で渡したデータは、popstate発生時にevent.stateから参照できます。これにより、どの履歴エントリーから戻ってきたのかを特定できます。
戻る・進む・リロードを判別するNavigation Timing APIの活用
「現在のページが表示された理由」が、通常のURL遷移(Navigate)なのか、ページの再読み込み(Reload)なのか、あるいはブラウザバック/進む(Back/Forward)によるものなのかを判定したい場合は、Navigation Timing API (Level 2)を使用します。
従来使われていたperformance.navigation.type(Level 1)は現在非推奨(Deprecated)となっており、最新の標準仕様ではperformance.getEntriesByType('navigation')を使用します。
/**
* ページのロードタイプ(遷移種別)を取得する判定関数
* @returns {'navigate' | 'reload' | 'back_forward' | 'prerender' | 'unknown'}
*/
function getPageNavigationType(){
// Navigation Timing API Level 2 のサポート確認
const navigationEntries = performance.getEntriesByType('navigation');
if (navigationEntries.length > 0) {
const navTiming = navigationEntries[0];
// navTiming.type には以下のいずれかが格納される:
// 'navigate': 通常のリンクアクセス、アドレスバー入力、フォーム送信
// 'reload': リロード(F5、更新ボタン)
// 'back_forward': ブラウザの「戻る」または「進む」による遷移
// 'prerender': プレレンダリングからの遷移
return navTiming.type;
}
return 'unknown';
}
// 実行例
window.addEventListener('DOMContentLoaded', () => {
const navType = getPageNavigationType();
switch (navType) {
case 'back_forward':
console.log('このページは「戻る」または「進む」ボタンで表示されました。');
// キャッシュデータの更新処理や、モーダル状態の復元処理を実行
break;
case 'reload':
console.log('このページは再読み込み(リロード)されました。');
break;
case 'navigate':
console.log('このページは通常の遷移で表示されました。');
break;
}
});
PerformanceNavigationTiming の主要プロパティ比較
| navTiming.type の値 | 発生シナリオ | 実務での活用例 |
|---|---|---|
'navigate' | 直リンク、検索結果からの流入、location.hrefによる遷移 | 通常アクセスとしてPVトラッキングや初期化を実行 |
'back_forward' | ブラウザの戻る・進むボタン、history.back() / forward() | BFCache非対応環境でのデータ最新化、スクロール位置復元 |
'reload' | F5キー押下、location.reload()、ブラウザ更新ボタン | 一時的なセッション状態のリセットまたは再データ読み込み |
標準仕様であるNavigation APIを使ったブラウザバック判定
従来のwindow.historyやpopstateは、シングルページアプリケーション(SPA)や高度な状態管理において非同期処理との同期が難しく、イベントのキャンセル挙動も直感的ではありませんでした。
これを解決するため、Web標準として新たに策定されたのがNavigation API(旧称: App History API)です。Chromium系ブラウザ(Chrome、Edge、Opera等)でフルサポートされており、ブラウザのあらゆる遷移(戻る、進む、リンククリック、フォーム送信)を単一の統合されたインターフェースでフックできます。
// Navigation API の利用可能チェック
if ('navigation' in window) {
// navigate イベントで全てのナビゲーションをキャッチ
window.navigation.addEventListener('navigate', (event) => {
// 1. 遷移の種類を判定
// event.navigationType には 'push', 'reload', 'replace', 'traverse' が入る
// 'traverse' は「戻る」「進む」など履歴間の移動を示す
if (event.navigationType === 'traverse') {
console.log('ブラウザバックまたは進むが発生しました。');
// 遷移先の履歴エントリー情報を確認
const destinationEntry = event.destination;
console.log('遷移先URL:', destinationEntry.url);
console.log('遷移先インデックス:', destinationEntry.index);
// 2. 必要に応じて非同期処理を実行しつつ遷移をインターセプト(中断・オーバーライド)
event.intercept({
async handler() {
// 例: 遷移前にサーバーへ状態を保存する、またはUIアニメーションを実行する
await saveCurrentStateData();
console.log('遷移前処理が完了しました。');
}
});
}
});
} else {
// Navigation API 非対応ブラウザ(Safari/Firefox等)向けのフォールバック処理
console.warn('Navigation API is not supported in this browser. Fallback to popstate.');
}
Navigation API のメリットと採用基準
event.navigationType === 'traverse': 「戻る/進む」を明快に判別できます。従来のpopstateのように事前にpushStateでダミー履歴を突っ込むようなトリックが不要になります。event.intercept()による非同期ハンドリング: 遷移が完了する前にPromiseを差し込み、非同期処理の完了を待機してから画面を切り替えることが可能です。- ブラウザサポート状況: Chrome/Edge等のChromium系ブラウザで標準機能として利用可能ですが、SafariやFirefoxでの完全対応までは
popstateによるフォールバック処理と組み合わせるプログレッシブ・エンハンスメント手法が推奨されます。
JavaScriptでブラウザバックを確実に検知して処理を実行する実装例
Webアプリケーションの開発現場では、単にブラウザバックを検知するだけでなく、「特定のUI状態(モーダルやドロワーメニュー)を閉じる」「非同期でデータを再取得して表示を更新する」といった具体的な処理と連携させるケースが圧倒的多数を占めます。
ここでは、実践でそのままコピー&ペーストして使える実用的なコードパターンを3つ解説します。

popstateだけで検知する簡単な実装
最もシンプルなパターンは、ページの離脱防止や特定アクションの発火を目的として、popstateイベントを監視する実装です。
以下のコードでは、ページ読み込み時にダミーの履歴エントリーを追加し、ユーザーが「戻る」ボタンを押したタイミングでイベントを捕捉して任意の処理を実行します。
<!-- HTML要素例 -->
<div id="status-message">画面を操作してください</div>
// ページ読み込み完了時に履歴を初期化
window.addEventListener('DOMContentLoaded', () => {
// 1. 現在のページにダミーの履歴エントリーを追加(現在のURLを変更せずにスタックを追加)
history.pushState({ page: 'initial' }, '', location.href);
// 2. popstate イベントリスナーの登録
window.addEventListener('popstate', (event) => {
// ブラウザバック発生時の処理
const statusEl = document.getElementById('status-message');
if (statusEl) {
statusEl.textContent = 'ブラウザバックが検知されました!';
}
console.log('ブラウザバック検知:', event.state);
// ページに留まらせたい場合は、再度 pushState を発行して履歴スタックを復元
// history.pushState({ page: 'initial' }, '', location.href);
});
});
実装のポイント
- 履歴スタックの初期状態:
pushStateを発行しないと、最初の戻るボタン押下時にそのまま前のページへ遷移してしまいます。必ずイベントリスナー登録前にエントリーを追加してください。 - 無駄な履歴追加の回避: 何度も
pushStateを実行すると、ユーザーが何度も「戻る」ボタンを押す必要が生じ、UX(ユーザー体験)が著しく低下します。必要最小限の制御に留めましょう。
モーダルやドロワーメニューの開閉状態とブラウザ履歴を連動させる
スマートフォン(iOS/Android)のWebアプリでは、モーダルウィンドウやドロワーメニューが開いている状態でユーザーが「戻る」ボタン(または画面端からのスワイプバック)を押した際、ページ全体がバックするのではなく「モーダルだけが閉じる」挙動が期待されます。
このUIパターンは、モーダル開閉とhistory.pushState() / history.back()を同期させることで直感的に実装できます。
<!-- モーダルUI構造 -->
<button id="open-modal-btn">モーダルを開く</button>
<div id="modal-overlay" class="modal" style="display: none;">
<div class="modal-content">
<h2>モーダルタイトル</h2>
<p>ブラウザバックを押すとこのモーダルだけが閉じます。</p>
<button id="close-modal-btn">閉じる</button>
</div>
</div>
class ModalController{
constructor(modalId) {
this.modal = document.getElementById(modalId);
this.isOpen = false;
this.init();
}
init() {
// popstate イベントの監視
window.addEventListener('popstate', (event) => {
// 履歴ステートが modal-open でない(戻るが押された)かつ モーダルが開いている場合
if (this.isOpen && (!event.state || !event.state.isModalOpen)) {
this.closeModal(false); // 履歴操作を伴わないクローズ
}
});
}
// モーダルを開く
openModal() {
if (this.isOpen) return;
this.isOpen = true;
this.modal.style.display = 'block';
// 1. モーダルが開いた状態を履歴スタックに追加
history.pushState({ isModalOpen: true }, '', location.href);
}
// モーダルを閉じる
// @param {boolean} triggerHistory - 手動で閉じた際に history.back() を呼ぶか
closeModal(triggerHistory = true) {
if (!this.isOpen) return;
this.isOpen = false;
this.modal.style.display = 'none';
// 2. ボタンクリック等で手動で閉じた場合は、追加した履歴エントリーを1つ戻す
if (triggerHistory && history.state && history.state.isModalOpen) {
history.back();
}
}
}
// 初期化とイベントの紐付け
document.addEventListener('DOMContentLoaded', () => {
const modalUI = new ModalController('modal-overlay');
document.getElementById('open-modal-btn').addEventListener('click', () => {
modalUI.openModal();
});
document.getElementById('close-modal-btn').addEventListener('click', () => {
modalUI.closeModal(true);
});
});
モーダル連動処理の動作メカニズム
| アクション | 履歴スタックの状態 | クラス内部の状態 (isOpen) | モーダル表示状態 |
|---|---|---|---|
| 初期状態 | [{ state: null }] | false | 非表示 |
| 「開く」ボタン押下 | [{ state: null }, { isModalOpen: true }] | true | 表示 |
| ブラウザバック押下 | [{ state: null }] に戻る (popstate発火) | false に切り替え | 非表示 (ページ遷移なし) |
| 「閉じる」ボタン押下 | history.back() を実行し [{ state: null }] へ | false に切り替え | 非表示 |
ブラウザバック発生時にAjaxでデータを再取得しスクロール位置を復元
ECサイトの一覧画面から詳細画面へ遷移し、ブラウザバックで戻ってきた際に「表示データが古いままである」「スクロール位置がトップに戻ってしまう」という問題が発生します。
この場合、popstate発生時にAjax(Fetch API)による最新データの差分取得と、sessionStorageを活用したスクロール位置の復元を組み合わせることで解決します。
// ページ離脱・戻り時のスクロール位置復元 & データ再取得モジュール
const ProductListManager = {
storageKey: 'scroll_position_list_page',
init() {
// 1. ページ離脱前に現在のスクロール位置を保存
window.addEventListener('beforeunload', () => {
sessionStorage.setItem(this.storageKey, window.scrollY.toString());
});
// 2. popstate(ブラウザバック)発生時の処理
window.addEventListener('popstate', async (event) => {
console.log('ブラウザバックによる復元を開始します。');
// 非同期で最新データを再取得(キャッシュ無効化オプションを指定)
await this.refreshProductList();
// スクロール位置の復元
this.restoreScrollPosition();
});
// 初期表示時のスクロール復元チェック(BFCache非対応環境向け)
this.restoreScrollPosition();
},
// Ajaxによるデータ最新化
async refreshProductList() {
try {
const response = await fetch('/api/products', {
headers: { 'Cache-Control': 'no-cache' }
});
if (!response.ok) throw new Error('ネットワークエラー');
const data = await response.json();
this.renderList(data);
} catch (error) {
console.error('データ更新に失敗しました:', error);
}
},
// DOM描画処理
renderList(products) {
const container = document.getElementById('product-list');
if (!container) return;
container.innerHTML = products.map(item => `
<div class="product-card" data-id="${item.id}">
<h3>${item.name}</h3>
<p>価格: ¥${item.price.toLocaleString()}</p>
</div>
`).join('');
},
// スクロール位置の復元
restoreScrollPosition() {
const savedPosition = sessionStorage.getItem(this.storageKey);
if (savedPosition !== null) {
// DOM描画完了後に指定位置へスクロール
requestAnimationFrame(() => {
window.scrollTo({
top: parseInt(savedPosition, 10),
behavior: 'instant' // 瞬時に移動
});
// 復元後はキーを削除
sessionStorage.removeItem(this.storageKey);
});
}
}
};
// 実行
document.addEventListener('DOMContentLoaded', () => ProductListManager.init());
実装上のハイライト
requestAnimationFrameの利用: AjaxによるDOMの書き換えが完了した直後にスクロール位置を復元しないと、ページ高さを計算できずスクロール移動が失敗します。レンダリングタイミングを同期するためにrequestAnimationFrameを活用するのがベストプラクティスです。sessionStorageの採用: タブを閉じるまで有効なsessionStorageを利用することで、別タブで同じページを開いた際の位置競合を防ぎます。
フォーム離脱や二重送信を防ぐ!ブラウザバック時の制御・警告テクニック
お問い合わせフォームの入力中や、ECサイトの決済・注文手続き中にユーザーが誤って「戻る」ボタンを押してしまうと、入力データの消失や注文データの二重送信といった深刻なトラブルにつながります。
ここでは、ブラウザバックに伴う離脱警告や注文画面での二重処理防止、そして「ブラウザバックの完全禁止」に関する現実的なアプローチを解説します。

beforeunloadイベントを利用して未保存データの確認ダイアログを表示
フォームに入力中の未保存データがある状態でユーザーがページを離れようとした場合、ブラウザの標準機能であるbeforeunloadイベントを使用して確認ダイアログを表示するのが標準的な解決策です。
// フォームの編集状態を管理するフラグ
let isFormDirty = false;
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('checkout-form');
if (!form) return;
// 1. フォーム内の入力フィールドに変更があった場合、フラグをONにする
form.addEventListener('input', () => {
isFormDirty = true;
});
// 2. フォーム送信時(正常な離脱)は警告を出さないようにフラグをOFFにする
form.addEventListener('submit', () => {
isFormDirty = false;
});
// 3. ページ離脱・ブラウザバック時の警告イベント設定
window.addEventListener('beforeunload', (event) => {
// フォームが編集されていない場合はダイアログを表示しない
if (!isFormDirty) return;
// モダンブラウザにおける標準的な離脱警告の設定方法
event.preventDefault();
// Chrome / Edge 等でダイアログ表示に必要(戻り値としてカスタムテキストを指定しても現代のブラウザでは無視され標準文面が表示される)
event.returnValue = '';
});
});
beforeunloadを実装する際の注意点とモダンブラウザの制限
- メッセージのカスタマイズ不可: かつては
event.returnValue = '変更が保存されていません';のように任意の文面を表示できましたが、悪質なサイトによるフィッシング防止等のセキュリティ上の理由から、現在の主要ブラウザ(Chrome, Safari, Firefox, Edge)では独自のテキスト指定が無効化され、ブラウザ固定の標準メッセージ(「このサイトを離れますか?」等)が表示されます。 - ユーザーの操作履歴が必要: ユーザーがページ内で一度もクリックやキー入力を行っていない場合、悪意のあるページ離脱防止を防ぐため、
beforeunloadによるダイアログ表示は発火しません。
決済画面でのブラウザバックによる意図しないAPI再処理を防止する
決済完了画面やフォーム送信完了画面でユーザーがブラウザバックを行い、再び処理が実行されてしまう(二重決済やリロードによる再送信)トラブルを防ぐには、フロントエンドのhistory.replaceState()とバックエンドでのワンタイムトークン認証を組み合わせます。
フロントエンド側では、処理完了時に履歴エントリーを上書き(replaceState)して完了画面へ置換することで、ブラウザバックによる送信直前画面への復帰を構造的に防ぎます。
<!-- 決済実行フォーム画面 -->
<form id="payment-form">
<input type="hidden" name="csrf_token" value="ONETIME_TOKEN_123456" />
<button type="submit" id="submit-btn">決済を確定する</button>
</form>
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('payment-form');
const submitBtn = document.getElementById('submit-btn');
if (!form) return;
form.addEventListener('submit', async (event) => {
event.preventDefault();
// 二重連打・連続送信を防止
submitBtn.disabled = true;
try {
// 決済API呼び出し(非同期通信)
const response = await fetch('/api/checkout', {
method: 'POST',
body: new FormData(form),
});
const result = await response.json();
if (result.success) {
// 【重要】送信成功時、現在の「入力画面」の履歴を「完了画面」のURLで完全に上書きする
// これにより、完了画面で戻るボタンを押しても送信直前の入力画面には戻らなくなる
history.replaceState(null, '', '/checkout/complete');
// 完了画面のDOM表示に切替(ページ遷移を行わない)
showCompleteScreen();
} else {
alert('決済処理に失敗しました。');
submitBtn.disabled = false;
}
} catch (error) {
console.error('通信エラー:', error);
submitBtn.disabled = false;
}
});
});
// 完了画面のDOM切り替え処理関数
function showCompleteScreen() {
const container = document.getElementById('checkout-container');
if (container) {
container.innerHTML = `
<div class="checkout-complete">
<h2>決済が正常に完了しました</h2>
<p>ご注文ありがとうございました。確認メールをお送りしました。</p>
</div>
`;
}
}
二重送信防止の多層防御アーキテクチャ
単一のJavaScript制御だけでなく、以下の3層で防ぐのがWebセキュリティのベストプラクティスです。
| 階層 | 対策技術・手法 | 役割と防護対象 |
|---|---|---|
| フロントエンド(UI) | ボタンのdisabled化 / replaceState | ユーザーの連打ミスおよびブラウザバックによる画面再表示の抑止 |
| 通信(HTTP) | Redirect After Post (PRGパターン) | POST送信後に303 Post RedirectでGETページへ飛ばし、F5リロードによる再送信を遮断 |
| バックエンド(API) | 冪等性キー(Idempotency Key) / ワンタイムトークン | 万一同じリクエストが複数回到達しても、API側で同一キーの2回目以降の処理を拒否 |
JavaScriptでブラウザバックを禁止・制御できるのか
結論から言うと、JavaScriptを使ってユーザーの「ブラウザバック行為」を100%完全に無効化(禁止)することは不可能です。
これはブラウザベンダー(Google, Apple, Mozilla等)が「ユーザーによるWebナビゲーションのコントロール権をWebサイト側が奪ってはならない」というアクセシビリティおよびセキュリティの理念を徹底しているためです。
完全に禁止できない技術的理由
- 履歴の消去APIは存在しない:
historyオブジェクトには要素を追加(pushState)または置換(replaceState)するメソッドしかなく、生成済みの過去のスタックを消去する機能はありません。 - 長押しによる履歴一覧からのジャンプ: マウスの戻るボタン長押しやアドレスバーの履歴メニューから複数ページ前へ一気にスキップされた場合、単一の
popstateトラップをすり抜ける可能性があります。 - セキュリティ保護機能: 画面の操作がない状態でループ的に
pushStateを実行して戻れなくする悪質な挙動(「戻るフック」「トラップ」)に対しては、各ブラウザのセキュリティ層が検知し、履歴ループを強制破棄する制限が実装されています。
現場での現実的な代替アプローチ
「ブラウザバックを禁止したい」という要件が発生した場合は、禁止しようとするのではなく「戻られたとしても破綻しない設計にする」ことが正解です。
- 履歴を増やさないモーダルやSPA設計にする: ページ遷移ではなくコンポーネントの表示切替で画面を構成する。
- セッションの有効期限・トークンチェックを厳格にする: ブラウザバックで古い画面に戻ったとしても、トークン切れとして処理し「この画面は期限切れです」と画面上に明示する。
popstateが動かない?検知できない原因とBFCache(キャッシュ)対策
「実装通りにpopstateを設定したはずなのに、ブラウザバック時にイベントが発火しない」「iOS Safariだけ動作が怪しい」といったトラブルは、開発現場で頻発する問題です。
これらの原因の多くは、単なるコードの記述ミスではなく、各ブラウザ固有のジェスチャー挙動や、パフォーマンス最適化機能であるBFCache(Back/Forward Cache)の仕様に起因しています。
ChromeやSafariの仕様差異とiOS・Androidのジェスチャー戻る挙動
popstateが正しく検知できない、または期待通りのタイミングで動作しない場合、まずは以下のブラウザ固有の仕様差異を確認してください。
1. 初回ページロード時の挙動(Chrome vs 古いSafari)
かつての古いブラウザ仕様では、ページの初期読み込み(ロード)直後にも自動でpopstateが発火していましたが、現在の標準仕様では「ユーザー操作やスクリプトによって履歴スタックが変化した時」のみ発火するよう統一されています。
初期表示時に予期せぬpopstateが走る場合は、document.readyStateがcompleteになる前にイベントリスナーが登録されているか、ブラウザのバージョンの違いが影響しています。
2. モバイル端末の「スワイプバック(ジェスチャー戻る)」の挙動
近年のiOS SafariやAndroid Chromeでは、画面端からのフリック操作による「スワイプバック」が主流です。この操作が行われた際、ブラウザはJavaScriptの実行を一時中断し、前画面のスクリーンショットをアニメーション表示しながら遷移します。
// スワイプバックによる画面遷移時、ジェスチャーの途中で popstate が発生するタイミング
window.addEventListener('popstate', (event) => {
// 注意: iOS Safariでは、スワイプアニメーションが完了するまで
// DOMの変更や重いJavaScript処理が画面描画に反映されない場合がある
console.log('ジェスチャーによるバックを検知');
});
この時、popstate内で重い同期処理(DOMの大量生成など)を実行すると、スワイプアニメーションのカクつき(フレーム落ち)が発生したり、イベントの到達自体がキャンセルされるリスクがあります。重い処理はrequestAnimationFrameやsetTimeoutを用いて非同期に逃がすのが鉄則です。
Back/Forward Cache(BFCache)からの復元をpageshowのpersistedで判定
「戻るボタンを押して前画面に戻った際、JavaScriptが再実行されず、表示内容やデータが更新されない」という現象の最大の原因はBFCache(Back/Forward Cache)です。
BFCacheとは?
BFCacheは、ユーザーがページを離れた際、JavaScriptの実行状態やDOM構造、メモリ内のデータをそのまま丸ごと保存(フリーズ)する仕組みです。再度「戻る」で戻ってきた際、ネットワーク通信や再レンダリングを行わず瞬時に画面を表示できる利点がありますが、通常のDOMContentLoadedやloadイベントが一切再発火しないという側面を持ちます。
【通常のページ遷移】
離脱 ──> サーバーリクエスト ──> DOM構築 ──> loadイベント発火 ──> JavaScript初期化
【BFCacheによる復元】
離脱 ──> (メモリ保存) ──(戻る)──> メモリから即復元 ──> pageshowイベント発火 (※loadは発火しない)
pageshowイベントと event.persisted による解決策
BFCacheから復元されたかどうかを正確に判定し、JavaScriptの再初期化や最新データの再取得を行うには、pageshowイベントのevent.persistedプロパティを監視します。
// ページ表示時に毎回発火する pageshow イベントを登録
window.addEventListener('pageshow', (event) => {
// event.persisted が true の場合、BFCache(キャッシュ)から復元されたことを示す
if (event.persisted) {
console.log('このページは BFCache から復元されました。データを再取得します。');
// 1. 最新データの再取得(Ajax)
fetchLatestData();
// 2. セッション状態やフォームの表示・UIの初期化
resetUIState();
} else {
console.log('通常の新規読み込み(初回アクセスまたはリロード)です。');
}
});
function fetchLatestData(){
// キャッシュを回避して最新のステータスを取得する処理
fetch('/api/user/status', { cache: 'no-store' })
.then(res => res.json())
.then(data => {
// DOMの更新処理
});
}
ブラウザバック時にloadやDOMContentLoadedが再実行されない理由
フロントエンド開発者が陥りがちな罠として、「ブラウザバックで戻ってきたらDOMContentLoaded内で定義したイベントリスナーや初期化処理がもう一度走るはずだ」という思い込みがあります。
前述の通り、BFCacheが有効な環境ではDOMContentLoadedもloadも再実行されません。 ページが破棄されておらず、単に「一時停止から再開」されただけだからです。
BFCacheを無効化・回避すべきか?
過去にはレスポンスヘッダーにCache-Control: no-storeを付与してBFCacheを強制無効化する手法もありましたが、Web Vitals(特にINPやLCPといったユーザー体験指標)の観点から、無闇にBFCacheを無効化することは非推奨となっています。
モダンな実装設計のガイドライン
| イベント名 | 発火タイミング | 役割・推奨される処理 |
|---|---|---|
DOMContentLoaded | 初回HTML解析完了時 | 初回のみ必要なDOM要素のバインド、イベントリスナーの一括登録 |
pageshow | ページ表示時(初回+BFCache復元時) | BFCache復元時の状態判定(event.persisted)、動的データの再取得 |
pagehide | ページ離脱時 | 実行中のタイマー停止、sessionStorageへのスクロール位置・入力状態の保存 |
DOMContentLoadedにすべての初期化処理を詰め込むのではなく、「ページの表示(復元含む)のたびに更新が必要な処理」はpageshow内に集約して設計することが、ブラウザバック検知における最も堅牢なアーキテクチャです。
よくある質問(FAQ)
JavaScriptによるブラウザバックの検知や制御に関して、開発現場でよく寄せられる代表的な疑問とその解決策を解説します。
-
戻るボタンを完全に無効化して、前のページに戻れないようにすることはできますか?
-
JavaScriptだけでブラウザの「戻る」機能を完全に無効化・禁止することは不可能です。
ブラウザのセキュリティおよびアクセシビリティ仕様上、ユーザーからページの移動権限を完全に奪うことはできない設計になっています。ただし、「戻るボタンを押されても同じページに留まらせる(ループさせる)」というUI上の挙動を作ることは可能です。
具体的には、ページ読み込み時に
history.pushState()で履歴を1つ追加しておき、popstateイベントが発火したタイミングで再度history.pushState()を実行して履歴を押し戻します。// 戻る操作を事実上キャンセルして同一ページに留める処理 window.addEventListener('DOMContentLoaded', () => { history.pushState(null, '', location.href); window.addEventListener('popstate', () => { // 戻るが押されたら即座に再度 pushState を行い、履歴を復元する history.pushState(null, '', location.href); alert('このページでは戻るボタンは利用できません。'); }); });注意: この手法はユーザーに強いストレスを与える可能性があるため、重要な入力フォームや決済処理など、確固たる理由がある場合のみ最小限で使用してください。
-
iOS Safariの「スワイプで戻る」ジェスチャーでもpopstateは正しく発火しますか?
-
はい、発火します。ただし実行タイミングや画面描画の挙動に注意が必要です。
iOS SafariやAndroid Chromeの画面端からのスワイプバック(ジェスチャー戻る)でも、内部的には通常のブラウザバックと同様に
popstateイベントが発火します。しかし、スワイプ操作中はブラウザが前画面のキャッシュ(BFCache)やスクリーンショットをアニメーション表示しているため、以下の点に留意する必要があります。
- 画面描画の遅延: スワイプ処理の最中に
popstate内で重いDOM操作や同期処理を実行すると、アニメーションがカクつく原因になります。 - 対話型ダイアログの制限: スワイプバック時に
confirm()などのダイアログを表示して遷移を止めようとしても、OSのジェスチャーUIと衝突して意図通りに動作しない場合があります。
スワイプバック時の処理は、視覚的アニメーションが完了するタイミングを考慮し、軽量な非同期処理に留めるのがベストプラクティスです。
- 画面描画の遅延: スワイプ処理の最中に
-
ReactやVue.jsなどのSPA(Single Page Application)でブラウザバックを検知・制御するにはどうすればよいですか?
-
画面遷移を管理している「ルーターライブラリ」のナビゲーションガード機能を利用するのが最も安全で確実です。
React RouterやVue RouterなどのSPAライブラリは、内部で
window.historyAPIを独自に拡張してルーティング状態を管理しています。そのため、生のpopstateイベントを直接登録すると、ライブラリ内部の状態管理と競合して意図しない挙動(2重遷移やルートの不整合)を引き起こす原因になります。SPA環境では、各フレームワークが提供する専用のフックやガード関数を使用してください。
Vue Router (v4) の場合
onBeforeRouteLeaveガードを使用することで、離脱前(ブラウザバック含む)にダイアログを表示して遷移を制御できます。import { onBeforeRouteLeave } from 'vue-router'; export default { setup() { onBeforeRouteLeave((to, from, next) => { const answer = window.confirm('編集中のデータがあります。離れてもよろしいですか?'); if (answer) { next(); // 遷移を許可 } else { next(false); // 遷移をキャンセル(ブラウザバックをブロック) } }); } };React (React Router v6) の場合
useBlockerフック(または利用中のライブラリに応じたブロッカー機能)を活用してルート遷移を補獲(フック)します。import { useBlocker } from 'react-router-dom'; function FormComponent(){ const [isDirty, setIsDirty] = useState(true); // フォームが編集状態(isDirty)の場合にブラウザバックや画面遷移をブロック useBlocker( ({ currentLocation, nextLocation }) => isDirty && currentLocation.pathname !== nextLocation.pathname ); return <form>...</form>; }
まとめ
JavaScriptにおけるブラウザバックの検知と制御は、WebアプリケーションのUI/UX改善やトラブル防止(データの二重送信・離脱防止)において極めて重要な要素です。
ブラウザのナビゲーション仕様(BFCacheやモバイルのジェスチャー戻るなど)を正しく理解し、要件に合わせた最適なAPIを組み合わせることで、ユーザーにとってストレスのない高品質なフロントエンド実装を実現しましょう。
WEBCOACH|副業・フリーランス特化型のオンラインWebデザインスクール


