Back

ブラウザタブが非アクティブになったことを検出する方法

ブラウザタブが非アクティブになったことを検出する方法

10秒ごとに発火するポーリング間隔、バックグラウンドで再生中の動画、ループ中のアニメーション ―― これらすべてが、5分前にタブを切り替えたユーザーのためにリソースを消費し続けています。適切なAPIを知ってさえいれば、解決は簡単です。

重要なポイント

  • Page Visibility API は、タブの可視性変化を検出するためのモダンで信頼性の高い方法であり、従来の window.onfocuswindow.onblur を用いたアプローチに代わるものです。
  • document.hidden または document.visibilityStatevisibilitychange イベントと組み合わせて使用することで、タブが非アクティブまたはアクティブになった際に反応できます。
  • 実用的な応用例として、ポーリングの一時停止、メディア再生の停止、フォーム下書きの自動保存、セッション終了時のアナリティクス送信などが挙げられます。
  • セッション終了の確実なトラッキングには、visibilitychangepagehide および navigator.sendBeacon() と組み合わせて使用し、back/forward キャッシュを破壊する unload イベントは避けましょう。
  • シングルページアプリケーションでは、メモリリークを防ぐために、コンポーネントの破棄時に必ずリスナーを削除してください。

適切なツール:Page Visibility API

従来のソリューションは、window の focus および blur イベント ―― window.onfocuswindow.onblur ―― に依存していました。これらは、ブラウザウィンドウがキーボードフォーカスを持っているかどうかを検出するもので、タブが実際に表示されているかどうかを判定するものではありません。分割画面や複数ウィンドウのワークフローでは、タブがフォーカスを持たずに完全に表示されていることもあれば、その逆もあり得ます。また、タブ切り替えに関しては、ブラウザ間で挙動が一貫していませんでした。

Page Visibility API は、この問題を適切に解決します。フォーカスの状態に関係なく、ページがユーザーから見えているかどうかを正確に通知してくれます。

現在の状態を取得するためのプロパティは2つあります:

  • document.visibilityState ―― "visible" または "hidden" を返します
  • document.hidden ―― タブが非表示の場合は true、表示中の場合は false を返します

モダンブラウザではベンダープレフィックスは不要です。この API は広範にサポート されており、デスクトップおよびモバイルの Chrome、Firefox、Safari、Edge で利用できます。

visibilitychange イベントのリッスン

visibilitychange イベント は、タブが非表示になったとき、または再び表示されたときに document 上で発火します。基本的なパターンは以下のとおりです:

document.addEventListener("visibilitychange", () => {
  if (document.hidden) {
    console.log("Tab is hidden — pause or reduce work");
  } else {
    console.log("Tab is visible — resume work");
  }
});

ブーリアンではなく文字列値が必要な場合は、document.visibilityState を直接読み取ることも可能です。

実用的なユースケース

ポーリングの一時停止と再開

ユーザーが別のタブにいる間に API リクエストを行うのではなく、インターバルを一時停止し、戻ってきたときに再開します:

let intervalId = null;

function startPolling() {
  if (intervalId !== null) return;
  intervalId = setInterval(fetchData, 10000);
}

function stopPolling() {
  clearInterval(intervalId);
  intervalId = null;
}

document.addEventListener("visibilitychange", () => {
  document.hidden ? stopPolling() : startPolling();
});

startPolling();

注意:ブラウザはバックグラウンドタブの setInterval を完全に停止するのではなく、スロットリングします。そのため、非表示タブでのタイミング精度に依存することはいずれにせよ信頼できません。明示的に一時停止する方がクリーンです。重複するインターバルへのガードを設けることで、startPolling が2回呼び出された場合に複数のタイマーが積み重なるのを防げます。

メディア再生の一時停止

const video = document.querySelector("video");

document.addEventListener("visibilitychange", () => {
  if (document.hidden) {
    video.pause();
  } else {
    video.play().catch(() => {
      // Autoplay may be blocked by the browser; handle silently
    });
  }
});

.catch() ハンドラは重要です。video.play() は Promise を返しますが、ブラウザが自動再生をブロックすると reject される可能性があり、これがないとキャッチされないエラーが発生してしまいます。

フォーム下書きの自動保存

固定タイマーではなく、ユーザーがタブを離れたタイミングで保存をトリガーします:

document.addEventListener("visibilitychange", () => {
  if (document.hidden) {
    saveDraft();
  }
});

ユーザーが離れる前にアナリティクスを送信

セッション終了時のデータについては、visibilitychange イベントpagehide と組み合わせ、ナビゲーションをブロックすることなくデータを確実に送信するために navigator.sendBeacon() を使用します:

document.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "hidden") {
    navigator.sendBeacon(
      "/analytics",
      JSON.stringify({ event: "tab_hidden" })
    );
  }
});

window.addEventListener("pagehide", () => {
  navigator.sendBeacon(
    "/analytics",
    JSON.stringify({ event: "page_unload" })
  );
});

この用途で unload イベントを使うのは避けてください ―― モダンブラウザでは信頼性が低く、back/forward キャッシュ(bfcache)を破壊してナビゲーションパフォーマンスを低下させます。モバイルブラウザでは、pagehidebeforeunload が常に発火するとは限らないため、visibilitychange で状態が "hidden" になることが、セッション終了を示す唯一の信頼できるシグナルとなることが多いです。

イベントリスナーのクリーンアップ

特にシングルページアプリケーションでは、不要になったリスナーは必ず削除してください:

function handleVisibility() {
  console.log(document.visibilityState);
}

document.addEventListener("visibilitychange", handleVisibility);

// Later, when tearing down:
document.removeEventListener("visibilitychange", handleVisibility);

removeEventListener に渡す参照は、addEventListener で使用したものとまったく同じ関数参照でなければならない点に注意してください。インラインのアロー関数は、毎回新しい参照を生成するため削除できません。

まとめ

Page Visibility API ―― 具体的には document.hiddendocument.visibilityState、および visibilitychange イベント ―― は、ブラウザタブが非アクティブになったことを検出するための正しいモダンな方法です。現行のすべてのブラウザで確実に動作し、window の focus および blur イベントでは不十分なタブ切り替えを正確に処理でき、セッション終了処理においては navigator.sendBeacon() および pagehide イベントとうまく組み合わせられます。これを活用して不要な処理を停止したり、状態を保存したり、エンゲージメントをトラッキングしたりし、終了時にはリスナーをクリーンアップしましょう。

FAQ

どちらもページが表示されているかどうかを報告しますが、返す型が異なります。document.hidden はブーリアンを返すため、簡潔な条件チェックに便利です。document.visibilityState は文字列(現在は visible または hidden)を返し、より説明的で、将来追加の状態が定義された場合にも対応できます。コードスタイルに合った方を使用してください。

ブラウザはバックグラウンドのタイマーを停止するのではなくスロットリングします。通常、1秒に1回程度かそれ以下に制限されます。インターバルは実行され続けますが、不規則なレートで動作するため、タイミングに敏感なロジックは信頼できなくなります。最もクリーンな解決策は、visibilitychange ハンドラ内で clearInterval を呼び出し、タブが再び表示されたときにインターバルを再開することです。

visibilitychange を pagehide および navigator.sendBeacon と組み合わせて使用することを推奨します。beforeunload や unload イベントはモバイルでは信頼性が低く、back/forward キャッシュを破壊してパフォーマンスを損ないます。状態が hidden の visibilitychange イベントは、デスクトップおよびモバイルブラウザを通じて、ユーザーがページを離れたことを検出するための最も一貫したシグナルです。

はい、ただし挙動は親に依存します。iframe は一般的に親ドキュメントの可視性状態を継承します。`display: none` などの CSS で iframe 自体を非表示にしても、iframe 内で visibilitychange イベントは発火しません。各 iframe は独自の document オブジェクトを持つため、トップウィンドウではなく iframe の document に対して visibilitychange をリッスンします。

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay