Edge 152でunloadイベントが発火しない理由と移行方法|155で既定無効化

Microsoft Edge 152でunloadイベントが発火しなくなった場合、原因はJavaScriptの不具合ではなく、Microsoft Edge Web Platformで始まった段階的な既定無効化です。Edge 152と153ではページ読み込みの60%、Edge 154では80%、Edge 155では100%で、サイトが明示的に許可しない限りunloadハンドラーが実行されなくなります。(Microsoft Learn)

そのため、ページ離脱時のデータ保存、アクセス解析の送信、WebSocketの切断、編集ロックの解除などをunloadだけに任せているサイトでは、処理が突然実行されない可能性があります。

恒久的な対策はunloadを再有効化することではありません。用途に応じてvisibilitychangepagehidepageshow、Page Lifecycle API、navigator.sendBeacon()fetch()keepaliveへ移行します。特に重要なのは、「ページ終了時に一度だけ処理する設計」から「処理途中で継続的に保存し、ページが非表示になった時点で最終チェックポイントを送る設計」へ変更することです。

目次

Edge 152でunloadイベントが発火しない理由

unloadは、文書が破棄される直前に実行されるイベントとして長く利用されてきました。

window.addEventListener('unload', () => {
  // ページ離脱時の処理
});

しかし、Edge 152以降ではunloadがPermissions Policyで制御されます。unloadが許可されていないページでは、addEventListener('unload', ...)を記述しても、登録処理が実質的に何もしない状態になります。JavaScriptの構文エラーになるわけではないため、コードレビューだけでは問題を見落としやすい点に注意が必要です。(Chromium)

Edge 152から155までの無効化スケジュール

Microsoftが公表している段階的な適用予定は次のとおりです。

EdgeバージョンStable公開時期既定で発火しなくなる割合
Edge 1522026年8月27日ページ読み込みの60%
Edge 1532026年9月10日の週ページ読み込みの60%
Edge 1542026年9月24日の週ページ読み込みの80%
Edge 1552026年10月8日の週ページ読み込みの100%

公開予定日はビルド状況などにより変更される可能性があります。Edge 152からは、メジャーバージョンのStableリリースが2週間間隔に変更されています。(Microsoft Learn)

ここでいう60%や80%は、単純に「全ユーザーの60%」という意味ではなく、ページ読み込みを単位とした段階的な適用です。そのため、開発者の端末では動作していても、別の利用者や別の環境ではすでに発火していない可能性があります。

一度テストして動いたことは、互換性が維持されている根拠にはなりません。

Edge 155でもAPI自体が即座に削除されるわけではない

Edge 155で100%になるのは、unload既定で許可されなくなる割合です。

Permissions Policyで明示的に許可したサイトや、企業向けポリシーを適用した管理端末では、当面の間unloadを再有効化できる可能性があります。ただし、MicrosoftはWebプラットフォームの長期的な方向性として、unloadのサポート自体を削除する方針を示しています。(Microsoft Learn)

したがって、Permissions Policyによる再有効化は移行期間を確保するための手段であり、恒久対応にはなりません。

unloadイベントが段階廃止される理由

ブラウザやOSがページを突然終了するから

unloadは、ページが正常な順序で終了することを前提としています。しかし、実際のブラウザでは次のような終了方法があります。

  • スマートフォンでブラウザをバックグラウンドにした後、OSがプロセスを終了する
  • メモリ不足により、非表示タブが破棄される
  • ブラウザやレンダラープロセスが異常終了する
  • ユーザーがタスク一覧からブラウザアプリを終了する
  • PCやスマートフォンが強制終了する
  • バッテリー切れやネットワーク切断が発生する

このような状況では、JavaScriptを実行する機会そのものがありません。unloadだけでなく、beforeunloadpagehideも必ず発火するとは限りません。

Microsoftも、データ保存、後処理、セッション終了の報告にunloadを使用する方法は信頼できないと説明しています。(Microsoft Learn)

bfcacheと仕組み上の相性が悪いから

bfcacheは、ユーザーがブラウザの「戻る」「進む」を操作したとき、以前のページを高速に復元する仕組みです。

ページがbfcacheへ保存される場合、そのページは完全には破棄されません。JavaScriptの状態や画面の状態を保持したまま、一時停止されることがあります。

一方、unloadハンドラーは「このページは完全に終了し、二度と復元されない」という前提で作られていることが多いため、bfcacheと根本的に衝突します。

デスクトップ版のブラウザでは、unloadリスナーがあることでページがbfcacheの対象外になり、「戻る」「進む」の表示が遅くなる場合があります。(web.dev)

つまり、unloadの廃止は互換性整理だけでなく、ページ表示性能を改善する目的もあります。

影響を受けやすい処理

自社コードにunloadが1か所でもあれば、処理内容を確認する必要があります。特に影響が大きいのは次の実装です。

unloadで行っている処理発火しない場合の症状推奨する移行方法
入力内容や下書きの保存フォーム内容が消える入力中の自動保存+visibilitychange
アクセス解析の送信離脱データや滞在時間が欠落するvisibilitychangesendBeacon()
セッション終了の通知オンライン状態が残り続けるハートビート+サーバー側タイムアウト
編集ロックの解除他の利用者が編集できなくなる有効期限付きロック、リース方式
WebSocketの切断処理サーバー側に古い接続情報が残る切断検知+タイムアウト+pagehide
タイマーやポーリングの停止バックグラウンドで処理が残るpagehidefreezeresume
iframeから親画面への終了通知親画面の状態が更新されないpagehide+応答タイムアウト
未保存データの警告離脱確認が表示されない条件付きのbeforeunload

自社のJavaScriptにunloadがなくても安心はできません。アクセス解析タグ、チャット、動画プレーヤー、広告、認証ライブラリ、古いUIフレームワークなどが、内部でunloadを登録している場合があります。

unloadの代わりに使用するイベントとAPI

unloadからの移行では、処理の目的に合ったイベントとAPIを選びます。単純にすべてをpagehideへ置き換える方法は適切ではありません。

イベント・API主な用途注意点
visibilitychange状態保存、分析データの送信hiddenは離脱確定ではない
pagehideナビゲーション、再読み込み、ページ終了の検知タブ切り替えだけでは発火しない
pageshowbfcacheから復元されたページの再開event.persistedで復元を判定できる
freezeタイマーや接続の一時停止主にChromium系のPage Lifecycle API
resume一時停止した処理の再開二重初期化を防ぐ必要がある
beforeunload未保存データがある場合の離脱警告常時登録しない
sendBeacon()少量のログや状態を非同期送信POSTが中心で、応答処理には向かない
fetch()keepaliveヘッダーやHTTPメソッドを指定した送信小さなリクエストに限定する

ChromeのPage Lifecycle APIでは、visibilityStatehiddenになった時点を、アプリや利用者の状態を保存する最後の信頼しやすいタイミングとして扱うことが推奨されています。pagehideはページ遷移を検知しつつ、bfcacheとの互換性を保てるイベントです。(Chrome for Developers)

visibilitychangeは「セッション終了」ではない

次の操作でもページはhiddenになります。

  • 別のタブへ移動した
  • ブラウザを最小化した
  • スマートフォンで別のアプリを開いた
  • 端末の画面をロックした
  • 別ページへ移動した

そのため、hiddenになっただけでログアウト処理や編集ロックの完全解除を行うと、ユーザーがページへ戻ったときに問題が発生します。

visibilitychangeでは「セッション終了」ではなく、現在の状態を保存するチェックポイント処理を実行してください。

unloadからPage Lifecycleへ移行する実装例

移行前の問題があるコード

次のコードは、ページ終了時にセッション情報を送信しています。

window.addEventListener('unload', () => {
  const payload = JSON.stringify({
    sessionId: currentSessionId,
    endedAt: new Date().toISOString()
  });

  navigator.sendBeacon('/api/session/end', payload);
});

Edge 152以降では、unloadが無効になったページ読み込みで、この処理全体が実行されません。

また、ページが一時的に非表示になっただけなのか、本当に終了したのかをクライアント側だけで正確に判定することもできません。

visibilitychangeとpagehideを使うコード

次の例では、visibilitychangeを優先し、発火しなかった環境をpagehideで補完しています。

let flushedWhileHidden = false;
let revision = 0;

function createCheckpoint(reason) {
  return {
    sessionId: currentSessionId,
    revision: ++revision,
    reason,
    pageUrl: location.href,
    savedAt: new Date().toISOString(),
    formState: collectCurrentFormState()
  };
}

function flushCheckpoint(reason) {
  const payload = JSON.stringify(createCheckpoint(reason));
  const blob = new Blob([payload], {
    type: 'application/json'
  });

  const queued = navigator.sendBeacon(
    '/api/session/checkpoint',
    blob
  );

  if (!queued) {
    void fetch('/api/session/checkpoint', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: payload,
      keepalive: true
    }).catch(() => {
      // 次回表示時に再送できるよう、必要に応じてローカルへ保存する
    });
  }
}

document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') {
    flushedWhileHidden = true;
    flushCheckpoint('hidden');
  } else {
    flushedWhileHidden = false;
  }
});

window.addEventListener('pagehide', () => {
  if (!flushedWhileHidden) {
    flushCheckpoint('pagehide');
  }
});

この実装で重要なのは、送信先を/api/session/endではなく、/api/session/checkpointとしている点です。

hiddenはセッション終了を保証しないため、サーバー側では「ユーザーが終了した」と確定させず、最新状態を更新するだけにします。

サーバー側では重複送信を前提にする

visibilitychangepagehideを併用すると、環境やタイミングによっては同じ内容が複数回送信される可能性があります。

サーバー側では、次のような対策を行います。

  • セッションIDとリビジョン番号を組み合わせて一意にする
  • 古いリビジョンの更新を無視する
  • 同一リクエストIDの処理を1回に限定する
  • 更新処理を冪等にする
  • 送信順序が前後しても壊れない設計にする

離脱時の通信は、ネットワーク切断やブラウザ終了により失敗する可能性があります。重要なデータは、ページ終了直前だけでなく、利用中にも継続的に保存してください。

用途別の具体的な移行方法

フォームや下書きは入力中に自動保存する

申込フォーム、CMS、業務システム、問い合わせ画面などでは、unload時の一括保存をやめます。

推奨する流れは次のとおりです。

  1. 入力内容が変化したら、一定時間後に自動保存する
  2. IndexedDBやlocalStorageにも下書きを保持する
  3. visibilitychangehiddenになったら、未保存分を送信する
  4. 次回表示時に未送信データがあれば再送する
  5. 保存成功後にローカルの未送信フラグを解除する

入力のたびに通信するとサーバー負荷が増えるため、300ミリ秒から1秒程度のデバウンスを入れる方法が一般的です。

ただし、注文確定、決済、申請提出などの重要処理は自動保存と分け、ユーザーの明示的な操作で確定させてください。

アクセス解析はチェックポイント方式にする

ページ離脱時に滞在時間や操作履歴をまとめて送る方式では、一部データの欠落を避けられません。

次のタイミングで小分けに送信すると、欠落の影響を抑えられます。

  • 一定時間ごと
  • 重要なボタンを押したとき
  • 画面遷移したとき
  • visibilityStatehiddenになったとき
  • SPAでルートが変わったとき

サーバー側では、最後に受信した時刻をもとにセッション終了を推定します。ページ終了イベントを、唯一のセッション終了判定にしてはいけません。

オンライン状態はハートビートとタイムアウトで判定する

チャット、共同編集、管理画面などで「オンライン」「編集中」を表示する場合、unloadでオフライン通知を送る設計は不安定です。

次の方式へ移行します。

  • クライアントが一定間隔でハートビートを送信する
  • サーバーが最終受信時刻を記録する
  • 一定時間更新がなければオフラインと判定する
  • WebSocket切断を検知した場合は早めに状態を更新する
  • 再接続時は同じセッションを復元できるようにする

pagehideやWebSocketの切断通知は、早期解除のための補助情報として使います。最終的な安全装置は、サーバー側のタイムアウトです。

編集ロックは有効期限付きにする

業務システムでよくある失敗が、unload時に編集ロックを解除する設計です。

ページが強制終了すると解除処理が走らず、他の利用者が長時間編集できなくなります。

編集ロックは次のようなリース方式にします。

  • ロック取得時に有効期限を設定する
  • 編集中は一定間隔で有効期限を延長する
  • 更新が止まったロックは自動失効させる
  • 明示的な「編集終了」操作では即時解除する
  • ページ終了時の解除通知は補助的に利用する

一時停止したリソースはpageshowで復元する

pagehideは、ページが完全に破棄される場合だけでなく、bfcacheへ保存される場合にも発火します。

次の値で、ブラウザがbfcacheへの保存を予定しているか確認できます。

window.addEventListener('pagehide', (event) => {
  if (event.persisted) {
    // bfcacheへ保存される可能性がある
  }
});

ただし、event.persistedtrueでも、最終的にキャッシュされることを完全には保証しません。

WebSocket、ポーリング、Web Locks、BroadcastChannelなどを停止した場合は、pageshowで復元します。

window.addEventListener('pageshow', (event) => {
  if (event.persisted) {
    restartConnections();
    refreshDisplayedData();
  }
});

復元時には、画面の状態が古くなっている可能性があります。サーバーから最新データを取得し、二重接続や二重タイマーが発生しないようにしてください。

beforeunloadは未保存データの警告だけに使う

beforeunloadは、unloadの代替としてすべてのページに登録するイベントではありません。

利用してよい代表的な場面は、ユーザーが入力途中の内容を失う可能性がある場合です。

let hasUnsavedChanges = false;

function warnBeforeLeave(event) {
  event.preventDefault();
  event.returnValue = '';
}

function setUnsavedChanges(value) {
  hasUnsavedChanges = value;

  if (hasUnsavedChanges) {
    window.addEventListener('beforeunload', warnBeforeLeave);
  } else {
    window.removeEventListener('beforeunload', warnBeforeLeave);
  }
}

常時登録するのではなく、未保存の変更が発生したときだけ追加し、保存完了後に削除します。

beforeunloadも、バックグラウンドで終了された場合などには発火しません。データ保存そのものは自動保存で行い、beforeunloadは警告表示に限定してください。(Chrome for Developers)

既存サイトがunloadに依存しているか確認する方法

ソースコードを検索する

まず、プロジェクト全体を次の文字列で検索します。

addEventListener('unload'
addEventListener("unload"
window.onunload
onunload=
.on('unload'
.on("unload"

次の場所も検索対象に含めてください。

  • ビルド前のソースコード
  • 圧縮・結合後のJavaScript
  • テンプレートファイル
  • WordPressテーマとプラグイン
  • タグマネージャーで配信しているスクリプト
  • iframe内で読み込んでいるコンテンツ
  • CDNから読み込んでいるライブラリ
  • npmパッケージやベンダーコード

圧縮されたJavaScriptしかない場合は、Edge DevToolsの整形機能を利用して呼び出し元を確認します。

Edge DevToolsのコンソールを確認する

unloadがPermissions Policyで拒否されると、コンソールに次のような警告が表示される場合があります。

[Violation] Permissions policy violation:
unload is not allowed in this document.

警告の右側に表示されるファイル名や行番号をクリックすると、unloadを登録しようとしたスクリプトを特定できます。

自社コードではなく、外部ライブラリやブラウザ拡張機能が原因になっている場合もあります。

Back/forward cacheのテストを実行する

Edge DevToolsで次の手順を実行します。

  1. 対象ページをMicrosoft Edgeで開く
  2. F12キーでDevToolsを開く
  3. Applicationパネルを開く
  4. Background services内のBack/forward cacheを選ぶ
  5. Test back/forward cacheを実行する
  6. 復元できない理由を確認する

unloadリスナーがbfcacheを妨げている場合、理由としてunload-listenerや類似した項目が表示されます。DevToolsの表記はバージョンによって変わる可能性があります。(Chrome for Developers)

Edge 155相当の状態を事前テストする方法

本番環境で問題が起きる前に、テスト環境でunloadを明示的に無効化します。

HTTPレスポンスヘッダーに次のPermissions Policyを設定してください。

Permissions-Policy: unload=()

空の許可リスト()を指定すると、トップページと配下のフレームでunloadの利用を拒否できます。Permissions Policyでは、feature=()がすべてのオリジンに対する拒否を意味します。(Chrome for Developers)

この状態で、少なくとも次の操作をテストします。

テスト操作確認する内容
通常のリンク移動データ保存やログ送信が継続するか
ブラウザの戻る・進む画面や通信が正しく復元されるか
ページの再読み込み二重登録や二重送信が起きないか
タブを切り替える不要なログアウトやロック解除が起きないか
タブを閉じる次回表示時に状態を回復できるか
ブラウザを終了するサーバー側タイムアウトが機能するか
スマートフォンでアプリを切り替える下書きや状態が保持されるか
オフラインにして終了する次回接続時に再送・復元できるか
戻る操作でbfcache復元するWebSocketやタイマーが再開するか

OSによるプロセス強制終了では、どのJavaScriptイベントも実行されない場合があります。そのため、テストでは「終了時の処理が動くか」だけでなく、「終了時の処理が一切動かなくても復旧できるか」を確認してください。

移行完了後は、本番環境にもPermissions-Policy: unload=()を設定すると、外部ライブラリや将来追加されるコードが再びunloadへ依存することを防げます。

unloadを一時的に再有効化する方法

すぐに改修できず、業務システムが停止するおそれがある場合は、Permissions Policyで同一オリジンのunloadを一時的に許可できます。

Permissions-Policy: unload=(self)

Permissions Policyのselfは、レスポンスを返したページと同じオリジンを表します。(Chrome for Developers)

外部iframeで許可する場合

外部オリジンのiframeにも許可する場合は、レスポンスヘッダーとiframeのallow属性の両方が必要です。

Permissions-Policy: unload=(self "https://legacy.example")
<iframe
  src="https://legacy.example"
  allow="unload">
</iframe>

iframe側のレスポンスや上位フレームにも、適切なPermissions Policyが必要になる場合があります。

ただし、再有効化しても次の問題は解消されません。

  • スマートフォンでは依然として発火しないことがある
  • OSによる強制終了では実行されない
  • ブラウザクラッシュでは実行されない
  • bfcacheの利用を妨げる可能性がある
  • 将来的な完全削除には対応できない

再有効化は、移行期限と担当者を決めたうえで利用してください。

管理端末ではEdgeポリシーによる一時回避も可能

企業や自治体などの管理対象端末では、Microsoft Edgeの次のポリシーを利用できます。

ForcePermissionPolicyUnloadDefaultEnabled

このポリシーを有効にすると、管理対象のEdgeでunloadが既定で有効な状態を維持できます。ただし、Webサイト側がより厳しいPermissions Policyを設定した場合は、サイト側の拒否が優先されることがあります。(Microsoft Learn)

この方法は、社内システムを緊急回避するための端末管理策です。

一般公開サイトの利用者には適用できず、管理外端末や他のブラウザに対する互換性も確保できません。ポリシーの適用と並行して、Webアプリ側の改修を進める必要があります。

unload移行で失敗しやすいポイント

unloadをpagehideへ機械的に置き換える

pagehideはbfcacheへ入る場合にも発火します。

そのため、次のような不可逆処理をそのまま移すと問題が起きます。

  • ログアウト
  • セッション削除
  • 編集データの破棄
  • サーバー側作業領域の削除
  • 認証トークンの無効化

pagehideでは、接続やタイマーを一時停止し、pageshowで復元できるようにします。

hiddenをログアウト扱いする

ユーザーが別タブへ移動しただけでログアウトされるサイトになってしまいます。

visibilitychangeではデータの保存やチェックポイント送信だけを行い、セッション終了はサーバー側のタイムアウトや明示的なログアウト操作で判断してください。

beforeunloadへすべて移す

beforeunloadも強制終了時には実行されません。

また、保存済みのページに常時登録すると、ブラウザの高速復元や利用者の操作性へ悪影響を与える場合があります。未保存データの警告だけに限定します。

通信が必ず成功すると考える

sendBeacon()fetch()keepaliveは、通常の非同期通信より離脱時の送信に適していますが、サーバーへの到達を絶対に保証する仕組みではありません。

次の設計を併用してください。

  • 重要データを早めに保存する
  • 未送信データをIndexedDBなどへ残す
  • 次回起動時に再送する
  • サーバー処理を冪等にする
  • リクエストへ一意なIDを付ける
  • 重要操作はユーザーの明示的な送信で確定する

外部スクリプトを確認しない

自社コードを修正しても、古い分析タグや外部ウィジェットがunloadを登録していることがあります。

DevToolsのコンソール、Networkパネル、bfcacheテストを使用し、読み込まれているスクリプト全体を確認してください。

実務で進める移行手順

unload対応は、次の順序で進めると影響範囲を整理しやすくなります。

  1. ソースコード、外部タグ、iframeからunload利用箇所を洗い出す
  2. 各ハンドラーが保存、通信、警告、後処理のどれを行っているか分類する
  3. テスト環境へPermissions-Policy: unload=()を設定する
  4. 重要なユーザー操作を一通り再現する
  5. データ保存を入力中の自動保存へ変更する
  6. 状態送信をvisibilitychangesendBeacon()へ移す
  7. 接続やタイマーをpagehidepageshow、Page Lifecycle APIへ移す
  8. セッションやロックをハートビートと有効期限方式へ変更する
  9. DevToolsでbfcacheへの保存と復元を確認する
  10. 本番リリース後、保存失敗率や送信欠落を監視する
  11. 問題がなければ本番でもunload=()を設定し、再依存を防止する

Edge 152ですでにunloadが発火しないページ読み込みが発生しています。まずはテスト環境で明示的に無効化し、影響箇所を特定してください。

Permissions Policyや企業ポリシーで一時的に再有効化することはできますが、最終的には「ページ終了を確実に検知する」という前提を捨てる必要があります。継続保存、再送、サーバー側タイムアウト、bfcacheからの復元を組み合わせ、終了イベントが一度も発火しなくても破綻しない設計へ移行することが、Edge 155以降にも通用する対策です。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次