Page Visibility APIを知る

こんにちは。

**「un-T factory! XA Advent Calendar 2022」** の2番バッターです。

しっかり送りバントを決めたいと思います。
よろしくおねがいします！

さて今回は、
**Page Visibility API**について知っていきたいと思います。

皆さん、Page Visibility APIについてどのくらいご存じでしょうか？

- 動画プレーヤーの制御
- スライドショーの実装時の自動再生設定
- GPUを使用して描画を行う際の処理制御

などで、考えたことがある方は少なからずいらっしゃるかもしれません、
あるいは、Page Visibility APIを認識していなくても
プロパティ`Document.visibilityState` やイベント`Document:visibilitychange`は
知っているぞ。という方もいらっしゃるかと思います。

本記事は上記についてまだしっかり意識したことがない、
主に初学者向けな内容になるかと思いますが、
進めていきます。

## **Page Visibility APIとは？**

まーこうゆうのはmdn web docsを読んでおこうってことで

[https://developer.mozilla.org/ja/docs/Web/API/Page_Visibility_API](https://developer.mozilla.org/ja/docs/Web/API/Page_Visibility_API)

> タブを使って閲覧している場合、どのウェブページもバックグラウンドにあってユーザーから見えていない場合があります。 Page Visibility API では、現在ページが見えているかどうかを調べる機能とともに、文書が表示されたり非表示になったりした時を監視することができるイベントを提供します。
ユーザーがウィンドウを最小化したり他のタブに切り替えたりした時、 API は `visibilitychange` イベントを送信してリスナーにページの状態が変化したことを知らせます。イベントを検出していくつかの操作を実行したり、様々な動作をしたりすることができます。例えば、ウェブアプリで動画を再生している場合、ユーザーがタブをバックグラウンドにした場合に動画を一時停止させ、ユーザーがこのタブに戻ったときに再生を再開させたりすることができます。ユーザーは動画の位置に迷うことがなく、動画の音声が新しく前景になったタブの音声を邪魔せず、ユーザーがその間に動画を見落とすことがなくなります。
> 

と書いてあります。
具体的な使用例としては、

- 画像のスライドショーがあるサイトで、ユーザーが見ていない間に次のスライドに進むべきではないもの
- 情報をダッシュボードに表示するアプリケーションで、ページが見えていないときは更新情報をサーバーへ問い合わせてほしくないもの
- 正確なページビューをカウントできるよう、ページがプリレンダリングされている状態を検出したい。
- デバイスがスタンバイモードである (ユーザーが電源ボタンを押して、画面を消灯している) ときに、音声を止めたいサイト

と記載されていますが、
先に書いたように、

- ユーザーが見ていない間は動画を一時停止したい
- GPUを使用した高負荷なアニメーションを実装する際に不必要なタスクの実行を抑止したい

など他にも利用できる局面は多いのかなと思ってます。

:::note warn
Page Visibility API とは別に、ユーザーエージェントは通常、バックグラウンドまたは非表示のタブのパフォーマンスへの影響を軽減するためのポリシーを数多く持っているようです。例えば、ほとんどのブラウザは、パフォーマンスとバッテリ寿命を向上させるために、バックグラウンド タブまたは非表示の iframe に requestAnimationFrame() コールバックを送信しないようにします。
:::
## 実際の使用法

さて実際の使用方法についてですが

【プロパティ】
`Document.hidden`もしくは`Document.visibilityState`

と

【イベント】
`Document.onvisibilitychange` 

を使用して実装していく形になるかと思います。
プロパティから見ていくと、

Document.hiddenとDocument.visibilityStateの違いについて、

Document.hiddenはページがユーザーから隠れた状態か否かを、
真偽値（true/false）で返します。

一方のDocument.visibilityStateは文書の現在の可視状態を示す、
DOMStringを返します。

返される値のパターンは以下の通りです。

`visible`

ページのコンテンツは少なくとも部分的に可視状態です。実際は、最小化されていないウィンドウのフォアグラウンドのタブにページがあることを意味します。

`hidden`

ページのコンテンツはユーザーから見えていません。実際は、文書がバックグラウンドのタブか最小化されているウィンドウにある、あるいは OS のスクリーンがロックされていることを意味します。

`prerender`

ページのコンテンツはプリレンダリングされており、ユーザーから見えていません (`document.hidden` では隠されているとみなされます)。文書は `prerender` の状態から始まるかもしれませんが、プリレンダリングは 1 つの文書は 1 回しか行われないので、他の状態からこの状態に移ることはありません。

:::note warn
すべてのブラウザーがプリレンダリングに対応しているわけではありません。
:::

`unloaded`

ページがメモリからアンロードされている途中です。

:::note warn
すべてのブラウザーが unloaded の値に対応しているわけではありません。
:::


上記の通り値にパターンがあり内いくつかの値はブラウザによってサポートされていないようなので、Document.visibilityStateほどの判定精度を要していない実装の場合は
Document.hiddenで良いのかなと思ってます。

続いてイベントを見ていきます。

といってもこちらはvisibilitychange イベントが発生したときに呼び出されるコードを提供する EventListener というだけです！

なので**visibilitychange イベントで文章の表示/非表示状態の変化を感知し、**
**実際の表示/非表示状態はDocument.hiddenもしくはDocument.visibilityStateで、**
**取得する**という流れになります。

※実装例

```jsx
//backgroundMusic は別途定義される

//Document.hiddenを使用する場合
document.addEventListener("visibilitychange", function() {
  // Opera 12.10 や Firefox 18 以前の対策をする場合
  // 考慮しないのであればdocument.hiddenのみで良い
  if (document.hidden || document.mozHidden || document.webkitHidden || document.msHidden) {
    //非表示状態なら停止
    backgroundMusic.pause();
  } else {
    //表示状態なら再生
    backgroundMusic.play();
  }
});

//Document.visibilityStateを使用する場合
document.addEventListener("visibilitychange", function() {
  if (document.visibilityState === 'visible') {
    //表示状態なら再生
    backgroundMusic.play();
  } else {
    //非表示状態なら停止
    backgroundMusic.pause();
  }
});
```

ちなみにvisibilitychange イベントは[W3C Recommendation](https://www.w3.org/TR/page-visibility/#sec-visibilitychange-event) には

> ユーザーエージェントは、トップレベルのブラウジングコンテキストが含むドキュメントの可視性が変化したと判断したとき、ドキュメントで visibilitychange イベントを発生させなければならない。（DeepL訳）
> 

と記載されています。

:::note info
親となるブラウジング・コンテキストがないブラウジング・コンテキストはそのブラウジング・コンテキスト自身とその子孫ブラウジング・コンテキストの“**最上位のブラウジング・コンテキスト**(top-level browsing context)”と言います。通常ブラウザのウインドウもしくはタブが直接含むブラウジング・コンテキストが最上位のブラウジング・コンテキストです。
:::

## onfocus / onblur イベントとの違い
ここまで聞いてイヤイヤ、ちょっと待ってくれよ。
onfocus / onblur でいいじゃないか。と思った方のために
違いを記載しておきます。

といっても`Window: focus` イベントと `Window: blur` イベントは
その名の通りフォーカス状態を表すものなので、
表示/非表示状態を表すものではありません。

一応CodePenにそれぞれの状態がわかるものを用意しました。

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="oNyMdbq" data-user="Shigematsu" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/Shigematsu/pen/oNyMdbq">
  three.js 22-2</a> by Keiichiro Shigematsu (<a href="https://codepen.io/Shigematsu">@Shigematsu</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

都合が良いのか悪いのか、エディター部分とプレビュー部分それぞれがiframeなので
onfocus / onblur イベントはエディター部分とプレビュー部分でフォーカスを切り替えるだけで発火します。（windowオブジェクトのイベントですしね。。）

フォーカスしたり、画面を最小化したり、タブを切り替えたりして、挙動を確認してみてください。


それと比較して、Document.onvisibilitychange イベントはページの表示/非表示情報を感知するので
最小化やタブの切り替えをしないと発火しません。
ちなみにページ非表示状態は"非表示"という書き込みがなされるはずですが、当然確認できません。
（念の為console.log記述もつっこんでいるので検証ツールで発火の確認はできます。）

他にも下記のような違いがあるようです。

**【Document.onvisibilitychange イベント】**

- Mac で、同じデスクトップ内の別アプリケーションに切り替えても発火しない
- スマホで別アプリやホーム画面への行き来で発火する

**【onfocus / onblur イベント】**

- Mac の同じデスクトップ内でも、別アプリケーションに移動すれば blur が発火する
- スマホで別アプリやホーム画面から戻ってくると focus が発火しない


組み合わせて実装する、ということも考えられますが、
CodePenで分かるようにonfocus / onblur イベントはwindow オブジェクトのイベントですので、**windowが複数存在するページでの制御には向いてない気がしてます。**



## まとめ

実装例を見ていただければ分かるように、実装はすごくシンプルで、
すぐにでも取り入れられるかと思います。

なので、
ユーザー体験の向上やページ表示中のパフォーマンス管理など
ちょっと意識を持ってガシガシ使っていきましょう〜

それでは皆さん、風邪など引かれぬよう体調管理をして
良き年末を過ごしましょう〜

## 参考サイト

[https://developer.mozilla.org/ja/docs/Web/API/Page_Visibility_API](https://developer.mozilla.org/ja/docs/Web/API/Page_Visibility_API)
[https://developer.mozilla.org/ja/docs/Web/API/Document/visibilitychange_event](https://developer.mozilla.org/ja/docs/Web/API/Document/visibilitychange_event)
[https://www.w3.org/TR/page-visibility/#sec-visibilitychange-event](https://www.w3.org/TR/page-visibility/#sec-visibilitychange-event)
[https://web.havincoffee.com/html/html5/html5_a_browsingcontext.html](https://web.havincoffee.com/html/html5/html5_a_browsingcontext.html)
https://zenn.dev/raihara3/articles/20220214_background_tab