ページを閉じたときやリロードしたときに動かしていたunloadイベントの処理が最近急に効かなくなった。そんな症状に心当たりはないでしょうか。
原因の一つがブラウザによるunloadイベントの段階的な無効化です。Microsoft Edgeでは2026年8月27日公開のEdge 152からサイト側で明示的に許可していないページの一部でunloadハンドラーが動かなくなる変更が進んでいます。
対応するにはunloadで何をしていたかに応じて処理を分けます。保存やアクセス解析の送信にはvisibilitychange、ページを離れる際の処理にはpagehide、未保存の変更を知らせる確認ダイアログにはbeforeunloadを使います。この記事ではそれぞれのコード例と一時的な回避方法をまとめます。仕様・計画は2026年9月確認時点の情報です。
Edge 152で何が変わったのか
Edge 152から進んでいるのはunloadハンドラーを既定で実行するかどうかの変更です。サイト側でPermissions Policyによる明示的な許可をしていないページでは段階的にunloadが発火しなくなります。
具体的な移行スケジュールは次の通りです。
| バージョン | unloadが無効になるページ読み込みの割合 |
|---|---|
| Edge 152、153 | 60パーセント |
| Edge 154 | 80パーセント |
| Edge 155 | 100パーセント |
Edge 154の一般提供は2026年9月24日ごろを予定しているとベータチャンネルのリリースノートに記載があります。ここは前後する可能性があるので、あくまで目安として見ておいてください。
割合ってなんですねんってカンジですが、これはページ読み込みに対する適用率です。同じバージョンでも適用状況やサイトの許可設定によって挙動が異なる可能性があります。一度unloadが動いたと確認がとれても、今回の変更の影響を受けないとは判断できないようですね。
そもそもunloadイベントとは
unloadはページがアンロードされるときに発火するイベントです。発火する場合はbeforeunloadやpagehideのあとに来ます。beforeunloadはリスナーを登録した場合に使うイベントで、unload自体も常に発火するわけではありません。ページを離れる操作とタブの切り替えによる違いを次の図にまとめました。


発火した時点でリソース自体はまだ存在しているもののユーザーからは何も見えていない状態です。window.openやalertのようなUI操作もすでに効きません。セッション終了時の保存処理やクリーンアップ、送信ログの記録など、いわゆる「最後の一仕事」を担わせる目的で長らく使われてきたイベントです。
なぜunloadは廃止に向かっているのか
MDNでもunloadはすでに非推奨として扱われていて使用を避けるよう明記されています。
モバイルでは以前から信頼できなかった
例えばスマホでページを開いたあと別アプリに切り替えそのままタスク管理画面からブラウザごと終了した場合、unloadイベントはそもそも発火しません。OSがアプリを強制終了するようなケースではブラウザ側にイベントを発火させる余地自体がないためです。
bfcacheを妨げる
unloadリスナーが登録されていると多くのブラウザはそのページをback forward cache、いわゆるbfcacheの対象から外します。bfcacheはページをメモリ上にまるごと保持しておき、戻る進むの操作を一瞬で完了させる仕組みです。unloadを使っているせいでユーザーが体感する戻る進むの速度が落ちてしまうというわけですね。
Chromeではもっと進んでいる
「これはEdge固有の話?」と思う方もいるかもしれませんが、この変更はChromiumプロジェクト側の仕様変更でChromeにもまったく同じ話が来ています。しかもタイミングで言うとChromeのほうが先行していて、この記事を書いている時点ではすでにEdgeより無効化が進んだ状態です。
| ブラウザ | バージョン | 無効化率 | 時期 |
|---|---|---|---|
| Chrome | 146 | 1パーセント | 2026年3月10日 |
| Chrome | 147 | 5パーセント | 2026年4月7日 |
| Chrome | 148 | 10パーセント | 2026年5月5日 |
| Chrome | 149 | 20パーセント | 2026年6月2日 |
| Chrome | 150 | 40パーセント | 2026年6月30日 |
| Chrome | 151 | 60パーセント | 2026年7月28日 |
| Chrome | 152 | 80パーセント | 2026年8月25日 |
| Chrome | 154 | 100パーセント | 2026年9月22日予定 |
| Edge | 152、153 | 60パーセント | 2026年8月27日から |
| Edge | 154 | 80パーセント | 2026年9月下旬予定 |
| Edge | 155 | 100パーセント | 未定 |
Chromeは2025年に上位50サイトを対象とした段階的な無効化を進め、2026年から対象を全サイトへ広げています。Edgeでは152・153でページ読み込みの60%を対象にすると案内されています。
FirefoxとSafariはこの一連の変更の対象外です。Firefoxはunloadリスナーが登録されたページをbfcacheの対象から外すという昔ながらの挙動を続けていますし、Safariはデスクトップも含めてもともとbfcacheを優先する設計になっています。今回denyへ寄せているのはChromiumベースのブラウザだけという点は押さえておくとよいでしょう。
beforeunloadに置き換えるだけでは解決しない
「unloadが使えないならbeforeunloadに乗り換えればいいのでは」と考えるかもですが、両者は役割が違うので単純な置き換えにはなりません。
beforeunloadはページがまだ表示されている段階で発火するキャンセル可能なイベントで、未保存の変更があるときに離脱確認ダイアログを出す用途に向いています。このイベントも「バックグラウンドタブが強制終了された場合には発火しない」という不安定さを抱えていて、今回の変更とは別に元から乱用が問題視されてきました。
必要なときだけリスナーを登録する書き方にしておくと余計な確認ダイアログを出さずに済みます。
下記は入力欄の現在値と保存済みの値を比較する例です。id=”name” の入力欄を用意し、その要素と初期値を設定したあとに実行してください。
const nameInput = document.querySelector('#name');
let savedValue = nameInput.value;
function handleBeforeUnload(event) {
event.preventDefault();
event.returnValue = true;
}
function updateBeforeUnload() {
const hasUnsavedChanges = (nameInput.value !== savedValue);
if (hasUnsavedChanges) {
window.addEventListener('beforeunload', handleBeforeUnload);
} else {
window.removeEventListener('beforeunload', handleBeforeUnload);
}
}
nameInput.addEventListener('input', updateBeforeUnload);
// 保存処理の成功後に、実際に保存された値を渡す
function markNameSaved(value) {
savedValue = value;
updateBeforeUnload();
}保存先から成功応答を受け取ったら実際に保存した値をmarkNameSaved()に渡します。送信ボタンを押した時点ではまだ保存済みとして扱いません。複数の入力欄があるフォームでは対象となる項目全体で変更の有無を判定してください。
確認ダイアログはユーザー操作などの条件を満たした場合に表示されます。表示文言はブラウザが決めるため独自の文章には変更できません。
離脱確認以外の「保存」や「送信」の代わりとしては次に紹介するvisibilitychangeやpagehideのほうが適しています。
代替手段の使い分け方
MDNやChromeのガイドで案内されている代替は主に二つです。用途に応じて使い分けます。
| イベント | 発火するタイミング | bfcacheへの影響 | 向いている用途 |
|---|---|---|---|
| visibilitychange | タブ切り替え、最小化、他ページへの遷移など | 影響なし | 非表示になった時点で、未保存データの保存や未送信ログの送信を試みる |
| pagehide | ページからの離脱、リロード、ウィンドウを閉じたときなど | 影響なし(bfcacheに保存される場合がある) | ページを離れる際の通知やリソースの停止・解放。復元時の再開も考慮する |
visibilitychangeを使うケース
document.visibilityStateがhiddenになるとページが非表示になったことを検知できます。タブの切り替えや最小化でも発火するため、この時点で未送信のデータを送る設計にします。ユーザーが同じページへ戻ることもあるので、タブを閉じたことやセッション終了が確定したとは扱いません。
document.addEventListener('visibilitychange', () => {
if (document.visibilityState !== 'hidden') {
return;
}
const json = {
event: "page_hidden",
timestamp: Date.now()
};
const body = new Blob(
[JSON.stringify(json)],
{ type: "application/json" }
);
const queued = navigator.sendBeacon('/api/analytics', body);
if (!queued) {
console.warn('解析データを送信キューに登録できませんでした');
}
});/api/analyticsは送信先の例です。同一オリジンにJSONを受け取る処理を用意してください。この例は非表示になるたびに送信するためセッション数としてそのまま集計しないようにします。
sendBeaconの戻り値がtrueでも確認できるのは送信キューへの登録までです。サーバーでの受信や保存の成功は確認できません。保存結果を確認する必要があるデータはページを操作している間にも保存してください。
pagehideを使うケース
pagehideは別ページへの移動やリロードなどで現在のページを離れる際の処理に使えます。ブラウザやOSによる強制終了では発火しない場合があるため、保存や終了通知が必ず実行されるとは限りません。
event.persistedがtrueの場合はページがbfcacheに保存される見込みです。離脱を記録したい場合は、この場合も含めて通知します。
window.addEventListener('pagehide', (event) => {
fetch('/api/page-hide', {
method: 'POST',
keepalive: true,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
event: 'page_hide',
timestamp: Date.now(),
persisted: event.persisted
})
})
.catch(() => {
// 離脱中は再試行できるとは限らないため、
// この通知だけで重要な状態を確定しない
});
});/api/page-hideは送信先の例です。同一オリジンに受信処理を用意してください。keepaliveはページを離れたあとも通信を継続できるようにする設定ですが、通信成功を保証するものではありません。この通知だけでログアウトやデータ削除を確定しないでください。
タイマーや接続の後片付けは必要なものを停止・解放し、bfcacheから戻った際に再開できるようにします。pageshowのevent.persistedがtrueならbfcacheから復元されたことを判定できます。
直前のvisibilitychangeの例と両方を使う場合は、同じ離脱で双方の通知が届く可能性を考慮してください。
今の挙動を一時的に維持する方法
移行が間に合わない場合や影響範囲をじっくり調査したい場合はPermissions-Policyヘッダーでunloadを明示的に許可しておく方法があります。
ここで注意したいのがPermissions-PolicyはHTTPレスポンスヘッダーとして送る必要があるという点です。CSPのようにmetaタグで指定する方法は用意されていないので、サーバー側やCDN側の設定を変更することになります。
- unload=self: 自分のオリジンで
unloadハンドラーの実行を許可します。 - unload=(): すべての読み込みで
unloadハンドラーを明示的に無効化します。先取りしてbfcacheの恩恵を受けたい場合に使います。 - unload=*: すべてのオリジンで許可します。埋め込みiframe側でも使い続けたい場合などに指定します。
iframe内で使う場合は親ページのPermissions Policyに加えてiframeのallow属性や子ページ側の設定も確認してください。親ページでunload=*を指定しても別の箇所で制限されていれば有効になりません。
Apacheの例はmod_headersが利用でき.htaccessでHeaderディレクティブを使える環境が前提です。すでにPermissions-Policyを設定している場合は既存の設定にunloadの指定を組み込み、ほかの制限を消さないようにしてください。以下のNginx・Expressの例でも既存ヘッダーの上書きに注意します。
Header set Permissions-Policy "unload=self"add_header Permissions-Policy "unload=self";app.use((req, res, next) => {
res.setHeader('Permissions-Policy', 'unload=self');
next();
});社内向けの業務システムなど組織としてもう少し猶予がほしい場合はForcePermissionPolicyUnloadDefaultEnabledという管理者向けポリシーも用意されています。EdgeとChromeの両方に対応しているので情シス部門と連携できる環境であれば選択肢に入れておくとよさそうです。これはあくまで移行期間を稼ぐための応急処置なので恒久的な対応としては代替イベントへの移行を進めるのが筋です。
自分のサイトへの影響を確認する方法
コードを直す前にまず今どれくらい影響を受けているかを確認しておきましょう。
DevToolsを開き「Applicationタブ」→「Back forward cache」→「Test back forward cache」を実行するとunloadハンドラーが原因でbfcacheの対象外になっているかどうかを教えてくれます。EdgeもChromiumベースなので同じ手順で確認できるはずですが、筆者側でのメニュー名の最終確認はまだできていません。ご自身の環境で一度試してみてください。
イベントの発火を比べる場合は、次のサンプルを使います。コンソールログで「Preserve log」を有効にしてからリロード、別ページへの移動、戻る操作、別タブへの切り替えを試してください。タブを閉じる操作はそのタブのDevToolsも閉じるため、このサンプルでのログ確認には向きません。
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>unload beforeunload pagehide visibilitychange 比較用</title>
</head>
<body>
<p>DevToolsのConsoleでPreserve logを有効にし、リロード、別ページへの移動、戻る操作、別タブへの切り替えを試してください。</p>
<script>
window.addEventListener('beforeunload', () => {
console.log('beforeunloadが発火しました。');
});
window.addEventListener('unload', () => {
console.log('unloadが発火しました。');
});
window.addEventListener('pagehide', (event) => {
console.log('pagehideが発火しました。persisted:', event.persisted);
});
document.addEventListener('visibilitychange', () => {
console.log('visibilitychangeが発火しました。visibilityState:', document.visibilityState);
});
</script>
</body>
</html>このサンプルはイベントの発火を比べるためのものです。unloadリスナーの登録自体がbfcacheの利用に影響する場合があります。移行後のbfcacheを確認するときはunloadリスナーを外したページでも試してください。
迷いやすいポイント
まとめ
- Edgeではunloadの既定無効化が進んでいて155では対象がページ読み込みの100%に広がる予定だよ。サイト側の明示的な許可などによる回避策とイベント自体の完全廃止は区別しておこう。
- 原因はunloadがそもそも信頼できないこととbfcacheを妨げてしまうことにあるよ。
- Chromeのほうが先にロールアウトが進んでいるので両方のブラウザで動作確認しておくと安心だよ。
- 保存や送信の処理はvisibilitychange、離脱そのものの検知はpagehideに置き換えるのが基本方針だよ。
- どうしても間に合わない場合はPermissions-Policyヘッダーで
unload=selfを指定して移行の時間を確保してね。
Windowsの設定やトラブル対処についてはこちらのまとめから関連記事を探せます。







